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