tokens.py 11 KB

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