base.py 26 KB

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