codekingpro/portable-devtools
114k
1"""2oauthlib.oauth2.rfc6749.parameters3~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~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-48"""9import json10import os11import time12import urllib.parse as urlparse13 14from oauthlib.common import add_params_to_qs, add_params_to_uri15from oauthlib.signals import scope_changed16 17from .errors import (18 InsecureTransportError, MismatchingStateError, MissingCodeError,19 MissingTokenError, MissingTokenTypeError, raise_from_error,20)21from .tokens import OAuth2Token22from .utils import is_secure_transport, list_to_scope, scope_to_list23 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 following30 parameters to the query component of the authorization endpoint URI31 using the ``application/x-www-form-urlencoded`` format as defined by32 [`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 after39 authorization as described in `Section 3.1.2`_.40 :param scope: The scope of the access request as described by41 `Section 3.3`_.42 :param state: An opaque value used by the client to maintain43 state between the request and callback. The authorization44 server includes this value when redirecting the user-agent45 back to the client. The parameter SHOULD be used for46 preventing cross-site request forgery as described in47 `Section 10.12`_.48 :param code_challenge: PKCE parameter. A challenge derived from the49 code_verifier that is sent in the authorization50 request, to be verified against later.51 :param code_challenge_method: PKCE parameter. A method that was used to derive the52 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:: http58 59 GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz60 &code_challenge=kjasBS523KdkAILD2k78NdcJSk2k3KHG6&code_challenge_method=S25661 &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.162 Host: server.example.com63 64 .. _`W3C.REC-html401-19991224`: https://tools.ietf.org/html/rfc6749#ref-W3C.REC-html401-1999122465 .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.266 .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.267 .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.368 .. _`section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.1269 """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 the97 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 parameters104 into. This may contain extra parameters. Default ''.105 106 :param include_client_id: `True` (default) to send the `client_id` in the107 body of the upstream request. This is required108 if the client is not authenticating with the109 authorization server as described in110 `Section 3.2.1`_.111 :type include_client_id: Boolean112 113 :param client_id: Unicode client identifier. Will only appear if114 `include_client_id` is True. *115 116 :param client_secret: Unicode client secret. Will only appear if set to a117 value that is not `None`. Invoking this function with118 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 previously122 obtained authorization code as the ``code`` argument. *123 124 :param redirect_uri: If the "redirect_uri" parameter was included in the125 authorization request as described in126 `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 the129 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 the134 function signature, but are specially documented arguments for items135 appearing in the generic `**kwargs` keyworded input.136 137 An example of an authorization code token request body:138 139 .. code-block:: http140 141 grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA142 &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb143 144 .. _`Section 4.1.1`: https://tools.ietf.org/html/rfc6749#section-4.1.1145 """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 request157 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 params167 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 parameters179 using the ``application/x-www-form-urlencoded`` format in the HTTP request180 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 token185 submitted for revocation. Clients MAY pass this186 parameter in order to help the authorization server187 to optimize the token lookup. If the server is188 unable to locate the token using the given hint, it189 MUST extend its search across all of its supported190 token types. An authorization server MAY ignore191 this parameter, particularly if it is able to detect192 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 this203 specification MAY define other values for this parameter using the204 registry defined in `Section 4.1.2`_.205 206 .. _`Section 1.4`: https://tools.ietf.org/html/rfc6749#section-1.4207 .. _`Section 1.5`: https://tools.ietf.org/html/rfc6749#section-1.5208 .. _`Section 4.1.2`: https://tools.ietf.org/html/rfc7009#section-4.1.2209 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, body228 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 authorization236 server issues an authorization code and delivers it to the client by237 adding the following parameters to the query component of the238 redirection URI using the ``application/x-www-form-urlencoded`` format:239 240 **code**241 REQUIRED. The authorization code generated by the242 authorization server. The authorization code MUST expire243 shortly after it is issued to mitigate the risk of leaks. A244 maximum authorization code lifetime of 10 minutes is245 RECOMMENDED. The client MUST NOT use the authorization code246 more than once. If an authorization code is used more than247 once, the authorization server MUST deny the request and SHOULD248 revoke (when possible) all tokens previously issued based on249 that authorization code. The authorization code is bound to250 the client identifier and redirection URI.251 252 **state**253 REQUIRED if the "state" parameter was present in the client254 authorization request. The exact value received from the255 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 by261 sending the following HTTP response:262 263 .. code-block:: http264 265 HTTP/1.1 302 Found266 Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA267 &state=xyz268 269 """270 if not is_secure_transport(uri):271 raise InsecureTransportError()272 273 query = urlparse.urlparse(uri).query274 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 params286 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 authorization292 server issues an access token and delivers it to the client by adding293 the following parameters to the fragment component of the redirection294 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 in301 Section 7.1. Value is case insensitive.302 303 **expires_in**304 RECOMMENDED. The lifetime in seconds of the access token. For305 example, the value "3600" denotes that the access token will306 expire in one hour from the time the response was generated.307 If omitted, the authorization server SHOULD provide the308 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 described313 by Section 3.3.314 315 **state**316 REQUIRED if the "state" parameter was present in the client317 authorization request. The exact value received from the318 client.319 320 :param uri:321 :param state:322 :param scope:323 324 Similar to the authorization code response, but with a full token provided325 in the URL fragment:326 327 .. code-block:: http328 329 HTTP/1.1 302 Found330 Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA331 &state=xyz&token_type=example&expires_in=3600332 """333 if not is_secure_transport(uri):334 raise InsecureTransportError()335 336 fragment = urlparse.urlparse(uri).fragment337 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'] = vin345 elif 'expires_in' in params:346 params.pop('expires_in')347 if vat:348 params['expires_at'] = vat349 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 params358 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 refresh364 token, and constructs the response by adding the following parameters365 to the entity body of the HTTP response with a 200 (OK) status code:366 367 access_token368 REQUIRED. The access token issued by the authorization server.369 token_type370 REQUIRED. The type of the token issued as described in371 `Section 7.1`_. Value is case insensitive.372 expires_in373 RECOMMENDED. The lifetime in seconds of the access token. For374 example, the value "3600" denotes that the access token will375 expire in one hour from the time the response was generated.376 If omitted, the authorization server SHOULD provide the377 expiration time via other means or document the default value.378 refresh_token379 OPTIONAL. The refresh token which can be used to obtain new380 access tokens using the same authorization grant as described381 in `Section 6`_.382 scope383 OPTIONAL, if identical to the scope requested by the client,384 otherwise REQUIRED. The scope of the access token as described385 by `Section 3.3`_.386 387 The parameters are included in the entity body of the HTTP response388 using the "application/json" media type as defined by [`RFC4627`_]. The389 parameters are serialized into a JSON structure by adding each390 parameter at the highest structure level. Parameter names and string391 values are included as JSON strings. Numerical values are included392 as JSON numbers. The order of parameters does not matter and can393 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:: http401 402 HTTP/1.1 200 OK403 Content-Type: application/json404 Cache-Control: no-store405 Pragma: no-cache406 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.1416 .. _`Section 6`: https://tools.ietf.org/html/rfc6749#section-6417 .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3418 .. _`RFC4627`: https://tools.ietf.org/html/rfc4627419 """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/267427 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'] = vin436 elif 'expires_in' in params:437 params.pop('expires_in')438 if vat:439 params['expires_at'] = vat440 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 params446 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 by460 # the client, the authorization server MUST include the "scope" response461 # parameter to inform the client of the actual scope granted.462 # https://tools.ietf.org/html/rfc6749#section-3.3463 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 = params471 w.old_scope = params.old_scopes472 w.new_scope = params.scopes473 raise w474 475def parse_expires(params):476 """Parse `expires_in`, `expires_at` fields from params477 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, else482 - convert into a float, else483 - reuse the same type as-is (usually string)484 - `_expires_at` is a special internal value returned to be always an `int`, based485 either on the presence of `expires_at`, or reuse the current time plus486 `expires_in`. This is typically used to validate token expiry.487 488 :param params: Dict with expires_in and expires_at optionally set489 :return: Tuple of `expires_in`, `expires_at`, and `_expires_at`. None if not set.490 """491 expires_in = None492 expires_at = None493 _expires_at = None494 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 int503 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_at513 elif isinstance(params.get('expires_at'), str):514 try:515 # Attempt to convert to int first, then float if int fails516 expires_at = int(params.get('expires_at'))517 _expires_at = expires_at518 except ValueError:519 try:520 expires_at = float(params.get('expires_at'))521 _expires_at = expires_at522 except ValueError:523 # no change from str524 expires_at = params.get('expires_at')525 if _expires_at is None and expires_in:526 expires_at = round(time.time()) + expires_in527 _expires_at = expires_at528 return expires_in, expires_at, _expires_at529 