parameters.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. """
  2. oauthlib.oauth2.rfc6749.parameters
  3. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  4. This module contains methods related to `Section 4`_ of the OAuth 2 RFC.
  5. .. _`Section 4`: https://tools.ietf.org/html/rfc6749#section-4
  6. """
  7. import json
  8. import os
  9. import time
  10. import urllib.parse as urlparse
  11. from oauthlib.common import add_params_to_qs, add_params_to_uri
  12. from oauthlib.signals import scope_changed
  13. from .errors import (
  14. InsecureTransportError, MismatchingStateError, MissingCodeError,
  15. MissingTokenError, MissingTokenTypeError, raise_from_error,
  16. )
  17. from .tokens import OAuth2Token
  18. from .utils import is_secure_transport, list_to_scope, scope_to_list
  19. def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None,
  20. scope=None, state=None, code_challenge=None, code_challenge_method='plain', **kwargs):
  21. """Prepare the authorization grant request URI.
  22. The client constructs the request URI by adding the following
  23. parameters to the query component of the authorization endpoint URI
  24. using the ``application/x-www-form-urlencoded`` format as defined by
  25. [`W3C.REC-html401-19991224`_]:
  26. :param uri:
  27. :param client_id: The client identifier as described in `Section 2.2`_.
  28. :param response_type: To indicate which OAuth 2 grant/flow is required,
  29. "code" and "token".
  30. :param redirect_uri: The client provided URI to redirect back to after
  31. authorization as described in `Section 3.1.2`_.
  32. :param scope: The scope of the access request as described by
  33. `Section 3.3`_.
  34. :param state: An opaque value used by the client to maintain
  35. state between the request and callback. The authorization
  36. server includes this value when redirecting the user-agent
  37. back to the client. The parameter SHOULD be used for
  38. preventing cross-site request forgery as described in
  39. `Section 10.12`_.
  40. :param code_challenge: PKCE parameter. A challenge derived from the
  41. code_verifier that is sent in the authorization
  42. request, to be verified against later.
  43. :param code_challenge_method: PKCE parameter. A method that was used to derive the
  44. code_challenge. Defaults to "plain" if not present in the request.
  45. :param kwargs: Extra arguments to embed in the grant/authorization URL.
  46. An example of an authorization code grant authorization URL:
  47. .. code-block:: http
  48. GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
  49. &code_challenge=kjasBS523KdkAILD2k78NdcJSk2k3KHG6&code_challenge_method=S256
  50. &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
  51. Host: server.example.com
  52. .. _`W3C.REC-html401-19991224`: https://tools.ietf.org/html/rfc6749#ref-W3C.REC-html401-19991224
  53. .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2
  54. .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2
  55. .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
  56. .. _`section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12
  57. """
  58. if not is_secure_transport(uri):
  59. raise InsecureTransportError()
  60. params = [(('response_type', response_type)),
  61. (('client_id', client_id))]
  62. if redirect_uri:
  63. params.append(('redirect_uri', redirect_uri))
  64. if scope:
  65. params.append(('scope', list_to_scope(scope)))
  66. if state:
  67. params.append(('state', state))
  68. if code_challenge is not None:
  69. params.append(('code_challenge', code_challenge))
  70. params.append(('code_challenge_method', code_challenge_method))
  71. for k in kwargs:
  72. if kwargs[k]:
  73. params.append((str(k), kwargs[k]))
  74. return add_params_to_uri(uri, params)
  75. def prepare_token_request(grant_type, body='', include_client_id=True, code_verifier=None, **kwargs):
  76. """Prepare the access token request.
  77. The client makes a request to the token endpoint by adding the
  78. following parameters using the ``application/x-www-form-urlencoded``
  79. format in the HTTP request entity-body:
  80. :param grant_type: To indicate grant type being used, i.e. "password",
  81. "authorization_code" or "client_credentials".
  82. :param body: Existing request body (URL encoded string) to embed parameters
  83. into. This may contain extra parameters. Default ''.
  84. :param include_client_id: `True` (default) to send the `client_id` in the
  85. body of the upstream request. This is required
  86. if the client is not authenticating with the
  87. authorization server as described in
  88. `Section 3.2.1`_.
  89. :type include_client_id: Boolean
  90. :param client_id: Unicode client identifier. Will only appear if
  91. `include_client_id` is True. *
  92. :param client_secret: Unicode client secret. Will only appear if set to a
  93. value that is not `None`. Invoking this function with
  94. an empty string will send an empty `client_secret`
  95. value to the server. *
  96. :param code: If using authorization_code grant, pass the previously
  97. obtained authorization code as the ``code`` argument. *
  98. :param redirect_uri: If the "redirect_uri" parameter was included in the
  99. authorization request as described in
  100. `Section 4.1.1`_, and their values MUST be identical. *
  101. :param code_verifier: PKCE parameter. A cryptographically random string that is used to correlate the
  102. authorization request to the token request.
  103. :param kwargs: Extra arguments to embed in the request body.
  104. Parameters marked with a `*` above are not explicit arguments in the
  105. function signature, but are specially documented arguments for items
  106. appearing in the generic `**kwargs` keyworded input.
  107. An example of an authorization code token request body:
  108. .. code-block:: http
  109. grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
  110. &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
  111. .. _`Section 4.1.1`: https://tools.ietf.org/html/rfc6749#section-4.1.1
  112. """
  113. params = [('grant_type', grant_type)]
  114. if 'scope' in kwargs:
  115. kwargs['scope'] = list_to_scope(kwargs['scope'])
  116. # pull the `client_id` out of the kwargs.
  117. client_id = kwargs.pop('client_id', None)
  118. if include_client_id:
  119. if client_id is not None:
  120. params.append(('client_id', client_id))
  121. # use code_verifier if code_challenge was passed in the authorization request
  122. if code_verifier is not None:
  123. params.append(('code_verifier', code_verifier))
  124. # the kwargs iteration below only supports including boolean truth (truthy)
  125. # values, but some servers may require an empty string for `client_secret`
  126. client_secret = kwargs.pop('client_secret', None)
  127. if client_secret is not None:
  128. params.append(('client_secret', client_secret))
  129. # this handles: `code`, `redirect_uri`, and other undocumented params
  130. for k in kwargs:
  131. if kwargs[k]:
  132. params.append((str(k), kwargs[k]))
  133. return add_params_to_qs(body, params)
  134. def prepare_token_revocation_request(url, token, token_type_hint="access_token",
  135. callback=None, body='', **kwargs):
  136. """Prepare a token revocation request.
  137. The client constructs the request by including the following parameters
  138. using the ``application/x-www-form-urlencoded`` format in the HTTP request
  139. entity-body:
  140. :param token: REQUIRED. The token that the client wants to get revoked.
  141. :param token_type_hint: OPTIONAL. A hint about the type of the token
  142. submitted for revocation. Clients MAY pass this
  143. parameter in order to help the authorization server
  144. to optimize the token lookup. If the server is
  145. unable to locate the token using the given hint, it
  146. MUST extend its search across all of its supported
  147. token types. An authorization server MAY ignore
  148. this parameter, particularly if it is able to detect
  149. the token type automatically.
  150. This specification defines two values for `token_type_hint`:
  151. * access_token: An access token as defined in [RFC6749],
  152. `Section 1.4`_
  153. * refresh_token: A refresh token as defined in [RFC6749],
  154. `Section 1.5`_
  155. Specific implementations, profiles, and extensions of this
  156. specification MAY define other values for this parameter using the
  157. registry defined in `Section 4.1.2`_.
  158. .. _`Section 1.4`: https://tools.ietf.org/html/rfc6749#section-1.4
  159. .. _`Section 1.5`: https://tools.ietf.org/html/rfc6749#section-1.5
  160. .. _`Section 4.1.2`: https://tools.ietf.org/html/rfc7009#section-4.1.2
  161. """
  162. if not is_secure_transport(url):
  163. raise InsecureTransportError()
  164. params = [('token', token)]
  165. if token_type_hint:
  166. params.append(('token_type_hint', token_type_hint))
  167. for k in kwargs:
  168. if kwargs[k]:
  169. params.append((str(k), kwargs[k]))
  170. headers = {'Content-Type': 'application/x-www-form-urlencoded'}
  171. if callback:
  172. params.append(('callback', callback))
  173. return add_params_to_uri(url, params), headers, body
  174. else:
  175. return url, headers, add_params_to_qs(body, params)
  176. def parse_authorization_code_response(uri, state=None):
  177. """Parse authorization grant response URI into a dict.
  178. If the resource owner grants the access request, the authorization
  179. server issues an authorization code and delivers it to the client by
  180. adding the following parameters to the query component of the
  181. redirection URI using the ``application/x-www-form-urlencoded`` format:
  182. **code**
  183. REQUIRED. The authorization code generated by the
  184. authorization server. The authorization code MUST expire
  185. shortly after it is issued to mitigate the risk of leaks. A
  186. maximum authorization code lifetime of 10 minutes is
  187. RECOMMENDED. The client MUST NOT use the authorization code
  188. more than once. If an authorization code is used more than
  189. once, the authorization server MUST deny the request and SHOULD
  190. revoke (when possible) all tokens previously issued based on
  191. that authorization code. The authorization code is bound to
  192. the client identifier and redirection URI.
  193. **state**
  194. REQUIRED if the "state" parameter was present in the client
  195. authorization request. The exact value received from the
  196. client.
  197. :param uri: The full redirect URL back to the client.
  198. :param state: The state parameter from the authorization request.
  199. For example, the authorization server redirects the user-agent by
  200. sending the following HTTP response:
  201. .. code-block:: http
  202. HTTP/1.1 302 Found
  203. Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
  204. &state=xyz
  205. """
  206. if not is_secure_transport(uri):
  207. raise InsecureTransportError()
  208. query = urlparse.urlparse(uri).query
  209. params = dict(urlparse.parse_qsl(query))
  210. if state and params.get('state', None) != state:
  211. raise MismatchingStateError()
  212. if 'error' in params:
  213. raise_from_error(params.get('error'), params)
  214. if not 'code' in params:
  215. raise MissingCodeError("Missing code parameter in response.")
  216. return params
  217. def parse_implicit_response(uri, state=None, scope=None):
  218. """Parse the implicit token response URI into a dict.
  219. If the resource owner grants the access request, the authorization
  220. server issues an access token and delivers it to the client by adding
  221. the following parameters to the fragment component of the redirection
  222. URI using the ``application/x-www-form-urlencoded`` format:
  223. **access_token**
  224. REQUIRED. The access token issued by the authorization server.
  225. **token_type**
  226. REQUIRED. The type of the token issued as described in
  227. Section 7.1. Value is case insensitive.
  228. **expires_in**
  229. RECOMMENDED. The lifetime in seconds of the access token. For
  230. example, the value "3600" denotes that the access token will
  231. expire in one hour from the time the response was generated.
  232. If omitted, the authorization server SHOULD provide the
  233. expiration time via other means or document the default value.
  234. **scope**
  235. OPTIONAL, if identical to the scope requested by the client,
  236. otherwise REQUIRED. The scope of the access token as described
  237. by Section 3.3.
  238. **state**
  239. REQUIRED if the "state" parameter was present in the client
  240. authorization request. The exact value received from the
  241. client.
  242. :param uri:
  243. :param state:
  244. :param scope:
  245. Similar to the authorization code response, but with a full token provided
  246. in the URL fragment:
  247. .. code-block:: http
  248. HTTP/1.1 302 Found
  249. Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA
  250. &state=xyz&token_type=example&expires_in=3600
  251. """
  252. if not is_secure_transport(uri):
  253. raise InsecureTransportError()
  254. fragment = urlparse.urlparse(uri).fragment
  255. params = dict(urlparse.parse_qsl(fragment, keep_blank_values=True))
  256. for key in ('expires_in',):
  257. if key in params: # cast things to int
  258. params[key] = int(params[key])
  259. if 'scope' in params:
  260. params['scope'] = scope_to_list(params['scope'])
  261. if 'expires_in' in params:
  262. params['expires_at'] = time.time() + int(params['expires_in'])
  263. if state and params.get('state', None) != state:
  264. raise ValueError("Mismatching or missing state in params.")
  265. params = OAuth2Token(params, old_scope=scope)
  266. validate_token_parameters(params)
  267. return params
  268. def parse_token_response(body, scope=None):
  269. """Parse the JSON token response body into a dict.
  270. The authorization server issues an access token and optional refresh
  271. token, and constructs the response by adding the following parameters
  272. to the entity body of the HTTP response with a 200 (OK) status code:
  273. access_token
  274. REQUIRED. The access token issued by the authorization server.
  275. token_type
  276. REQUIRED. The type of the token issued as described in
  277. `Section 7.1`_. Value is case insensitive.
  278. expires_in
  279. RECOMMENDED. The lifetime in seconds of the access token. For
  280. example, the value "3600" denotes that the access token will
  281. expire in one hour from the time the response was generated.
  282. If omitted, the authorization server SHOULD provide the
  283. expiration time via other means or document the default value.
  284. refresh_token
  285. OPTIONAL. The refresh token which can be used to obtain new
  286. access tokens using the same authorization grant as described
  287. in `Section 6`_.
  288. scope
  289. OPTIONAL, if identical to the scope requested by the client,
  290. otherwise REQUIRED. The scope of the access token as described
  291. by `Section 3.3`_.
  292. The parameters are included in the entity body of the HTTP response
  293. using the "application/json" media type as defined by [`RFC4627`_]. The
  294. parameters are serialized into a JSON structure by adding each
  295. parameter at the highest structure level. Parameter names and string
  296. values are included as JSON strings. Numerical values are included
  297. as JSON numbers. The order of parameters does not matter and can
  298. vary.
  299. :param body: The full json encoded response body.
  300. :param scope: The scope requested during authorization.
  301. For example:
  302. .. code-block:: http
  303. HTTP/1.1 200 OK
  304. Content-Type: application/json
  305. Cache-Control: no-store
  306. Pragma: no-cache
  307. {
  308. "access_token":"2YotnFZFEjr1zCsicMWpAA",
  309. "token_type":"example",
  310. "expires_in":3600,
  311. "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
  312. "example_parameter":"example_value"
  313. }
  314. .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1
  315. .. _`Section 6`: https://tools.ietf.org/html/rfc6749#section-6
  316. .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
  317. .. _`RFC4627`: https://tools.ietf.org/html/rfc4627
  318. """
  319. try:
  320. params = json.loads(body)
  321. except ValueError:
  322. # Fall back to URL-encoded string, to support old implementations,
  323. # including (at time of writing) Facebook. See:
  324. # https://github.com/oauthlib/oauthlib/issues/267
  325. params = dict(urlparse.parse_qsl(body))
  326. for key in ('expires_in',):
  327. if key in params: # cast things to int
  328. params[key] = int(params[key])
  329. if 'scope' in params:
  330. params['scope'] = scope_to_list(params['scope'])
  331. if 'expires_in' in params:
  332. if params['expires_in'] is None:
  333. params.pop('expires_in')
  334. else:
  335. params['expires_at'] = time.time() + int(params['expires_in'])
  336. params = OAuth2Token(params, old_scope=scope)
  337. validate_token_parameters(params)
  338. return params
  339. def validate_token_parameters(params):
  340. """Ensures token presence, token type, expiration and scope in params."""
  341. if 'error' in params:
  342. raise_from_error(params.get('error'), params)
  343. if not 'access_token' in params:
  344. raise MissingTokenError(description="Missing access token parameter.")
  345. if not 'token_type' in params:
  346. if os.environ.get('OAUTHLIB_STRICT_TOKEN_TYPE'):
  347. raise MissingTokenTypeError()
  348. # If the issued access token scope is different from the one requested by
  349. # the client, the authorization server MUST include the "scope" response
  350. # parameter to inform the client of the actual scope granted.
  351. # https://tools.ietf.org/html/rfc6749#section-3.3
  352. if params.scope_changed:
  353. message = 'Scope has changed from "{old}" to "{new}".'.format(
  354. old=params.old_scope, new=params.scope,
  355. )
  356. scope_changed.send(message=message, old=params.old_scopes, new=params.scopes)
  357. if not os.environ.get('OAUTHLIB_RELAX_TOKEN_SCOPE', None):
  358. w = Warning(message)
  359. w.token = params
  360. w.old_scope = params.old_scopes
  361. w.new_scope = params.scopes
  362. raise w