base.py 20 KB

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