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