Merge pull request #3 from guyrt/master
[twitter-api-cdsw-solutions] / oauthlib / oauth2 / rfc6749 / tokens.py
1 """
2 oauthlib.oauth2.rfc6749.tokens
3 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
5 This module contains methods for adding two types of access tokens to requests.
6
7 - Bearer http://tools.ietf.org/html/rfc6750
8 - MAC http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
9 """
10 from __future__ import absolute_import, unicode_literals
11
12 from binascii import b2a_base64
13 import hashlib
14 import hmac
15 try:
16     from urlparse import urlparse
17 except ImportError:
18     from urllib.parse import urlparse
19
20 from oauthlib.common import add_params_to_uri, add_params_to_qs, unicode_type
21 from oauthlib import common
22
23 from . import utils
24
25
26 class OAuth2Token(dict):
27
28     def __init__(self, params, old_scope=None):
29         super(OAuth2Token, self).__init__(params)
30         self._new_scope = None
31         if 'scope' in params:
32             self._new_scope = set(utils.scope_to_list(params['scope']))
33         if old_scope is not None:
34             self._old_scope = set(utils.scope_to_list(old_scope))
35             if self._new_scope is None:
36                 # the rfc says that if the scope hasn't changed, it's optional
37                 # in params so set the new scope to the old scope
38                 self._new_scope = self._old_scope
39         else:
40             self._old_scope = self._new_scope
41
42     @property
43     def scope_changed(self):
44         return self._new_scope != self._old_scope
45
46     @property
47     def old_scope(self):
48         return utils.list_to_scope(self._old_scope)
49
50     @property
51     def old_scopes(self):
52         return list(self._old_scope)
53
54     @property
55     def scope(self):
56         return utils.list_to_scope(self._new_scope)
57
58     @property
59     def scopes(self):
60         return list(self._new_scope)
61
62     @property
63     def missing_scopes(self):
64         return list(self._old_scope - self._new_scope)
65
66     @property
67     def additional_scopes(self):
68         return list(self._new_scope - self._old_scope)
69
70
71 def prepare_mac_header(token, uri, key, http_method,
72                        nonce=None,
73                        headers=None,
74                        body=None,
75                        ext='',
76                        hash_algorithm='hmac-sha-1',
77                        issue_time=None,
78                        draft=0):
79     """Add an `MAC Access Authentication`_ signature to headers.
80
81     Unlike OAuth 1, this HMAC signature does not require inclusion of the
82     request payload/body, neither does it use a combination of client_secret
83     and token_secret but rather a mac_key provided together with the access
84     token.
85
86     Currently two algorithms are supported, "hmac-sha-1" and "hmac-sha-256",
87     `extension algorithms`_ are not supported.
88
89     Example MAC Authorization header, linebreaks added for clarity
90
91     Authorization: MAC id="h480djs93hd8",
92                        nonce="1336363200:dj83hs9s",
93                        mac="bhCQXTVyfj5cmA9uKkPFx1zeOXM="
94
95     .. _`MAC Access Authentication`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
96     .. _`extension algorithms`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-7.1
97
98     :param uri: Request URI.
99     :param headers: Request headers as a dictionary.
100     :param http_method: HTTP Request method.
101     :param key: MAC given provided by token endpoint.
102     :param hash_algorithm: HMAC algorithm provided by token endpoint.
103     :param issue_time: Time when the MAC credentials were issued (datetime).
104     :param draft: MAC authentication specification version.
105     :return: headers dictionary with the authorization field added.
106     """
107     http_method = http_method.upper()
108     host, port = utils.host_from_uri(uri)
109
110     if hash_algorithm.lower() == 'hmac-sha-1':
111         h = hashlib.sha1
112     elif hash_algorithm.lower() == 'hmac-sha-256':
113         h = hashlib.sha256
114     else:
115         raise ValueError('unknown hash algorithm')
116
117     if draft == 0:
118         nonce = nonce or '{0}:{1}'.format(utils.generate_age(issue_time),
119                                           common.generate_nonce())
120     else:
121         ts = common.generate_timestamp()
122         nonce = common.generate_nonce()
123
124     sch, net, path, par, query, fra = urlparse(uri)
125
126     if query:
127         request_uri = path + '?' + query
128     else:
129         request_uri = path
130
131     # Hash the body/payload
132     if body is not None and draft == 0:
133         body = body.encode('utf-8')
134         bodyhash = b2a_base64(h(body).digest())[:-1].decode('utf-8')
135     else:
136         bodyhash = ''
137
138     # Create the normalized base string
139     base = []
140     if draft == 0:
141         base.append(nonce)
142     else:
143         base.append(ts)
144         base.append(nonce)
145     base.append(http_method.upper())
146     base.append(request_uri)
147     base.append(host)
148     base.append(port)
149     if draft == 0:
150         base.append(bodyhash)
151     base.append(ext or '')
152     base_string = '\n'.join(base) + '\n'
153
154     # hmac struggles with unicode strings - http://bugs.python.org/issue5285
155     if isinstance(key, unicode_type):
156         key = key.encode('utf-8')
157     sign = hmac.new(key, base_string.encode('utf-8'), h)
158     sign = b2a_base64(sign.digest())[:-1].decode('utf-8')
159
160     header = []
161     header.append('MAC id="%s"' % token)
162     if draft != 0:
163         header.append('ts="%s"' % ts)
164     header.append('nonce="%s"' % nonce)
165     if bodyhash:
166         header.append('bodyhash="%s"' % bodyhash)
167     if ext:
168         header.append('ext="%s"' % ext)
169     header.append('mac="%s"' % sign)
170
171     headers = headers or {}
172     headers['Authorization'] = ', '.join(header)
173     return headers
174
175
176 def prepare_bearer_uri(token, uri):
177     """Add a `Bearer Token`_ to the request URI.
178     Not recommended, use only if client can't use authorization header or body.
179
180     http://www.example.com/path?access_token=h480djs93hd8
181
182     .. _`Bearer Token`: http://tools.ietf.org/html/rfc6750
183     """
184     return add_params_to_uri(uri, [(('access_token', token))])
185
186
187 def prepare_bearer_headers(token, headers=None):
188     """Add a `Bearer Token`_ to the request URI.
189     Recommended method of passing bearer tokens.
190
191     Authorization: Bearer h480djs93hd8
192
193     .. _`Bearer Token`: http://tools.ietf.org/html/rfc6750
194     """
195     headers = headers or {}
196     headers['Authorization'] = 'Bearer %s' % token
197     return headers
198
199
200 def prepare_bearer_body(token, body=''):
201     """Add a `Bearer Token`_ to the request body.
202
203     access_token=h480djs93hd8
204
205     .. _`Bearer Token`: http://tools.ietf.org/html/rfc6750
206     """
207     return add_params_to_qs(body, [(('access_token', token))])
208
209
210 def random_token_generator(request, refresh_token=False):
211     return common.generate_token()
212
213
214 def signed_token_generator(private_pem, **kwargs):
215     def signed_token_generator(request):
216         request.claims = kwargs
217         return common.generate_signed_token(private_pem, request)
218
219     return signed_token_generator
220
221
222 class TokenBase(object):
223
224     def __call__(self, request, refresh_token=False):
225         raise NotImplementedError('Subclasses must implement this method.')
226
227     def validate_request(self, request):
228         raise NotImplementedError('Subclasses must implement this method.')
229
230     def estimate_type(self, request):
231         raise NotImplementedError('Subclasses must implement this method.')
232
233
234 class BearerToken(TokenBase):
235
236     def __init__(self, request_validator=None, token_generator=None,
237                  expires_in=None, refresh_token_generator=None):
238         self.request_validator = request_validator
239         self.token_generator = token_generator or random_token_generator
240         self.refresh_token_generator = (
241             refresh_token_generator or self.token_generator
242         )
243         self.expires_in = expires_in or 3600
244
245     def create_token(self, request, refresh_token=False):
246         """Create a BearerToken, by default without refresh token."""
247
248         if callable(self.expires_in):
249             expires_in = self.expires_in(request)
250         else:
251             expires_in = self.expires_in
252
253         request.expires_in = expires_in
254
255         token = {
256             'access_token': self.token_generator(request),
257             'expires_in': expires_in,
258             'token_type': 'Bearer',
259         }
260
261         if request.scopes is not None:
262             token['scope'] = ' '.join(request.scopes)
263
264         if request.state is not None:
265             token['state'] = request.state
266
267         if refresh_token:
268             if (request.refresh_token and
269                     not self.request_validator.rotate_refresh_token(request)):
270                 token['refresh_token'] = request.refresh_token
271             else:
272                 token['refresh_token'] = self.refresh_token_generator(request)
273
274         token.update(request.extra_credentials or {})
275         token = OAuth2Token(token)
276         self.request_validator.save_bearer_token(token, request)
277         return token
278
279     def validate_request(self, request):
280         token = None
281         if 'Authorization' in request.headers:
282             token = request.headers.get('Authorization')[7:]
283         else:
284             token = request.access_token
285         return self.request_validator.validate_bearer_token(
286             token, request.scopes, request)
287
288     def estimate_type(self, request):
289         if request.headers.get('Authorization', '').startswith('Bearer'):
290             return 9
291         elif request.access_token is not None:
292             return 5
293         else:
294             return 0

Benjamin Mako Hill || Want to submit a patch?