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

1# -*- coding: utf-8 -*- 

2""" 

3oauthlib.oauth2.rfc6749 

4~~~~~~~~~~~~~~~~~~~~~~~ 

5 

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 

15 

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 

26 

27AUTH_HEADER = 'auth_header' 

28URI_QUERY = 'query' 

29BODY = 'body' 

30 

31FORM_ENC_HEADERS = { 

32 'Content-Type': 'application/x-www-form-urlencoded' 

33} 

34 

35 

36class Client: 

37 """Base OAuth2 client responsible for access token management. 

38 

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. 

46 

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`. 

52 

53 """ 

54 refresh_token_key = 'refresh_token' 

55 

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. 

73 

74 :param client_id: Client identifier given by the OAuth provider upon 

75 registration. 

76 

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``). 

80 

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. 

85 

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. 

89 

90 :param refresh_token: A refresh token (string) used to refresh expired 

91 tokens. Can also be supplied inside the ``token`` dict parameter. 

92 

93 :param mac_key: Encryption key used with MAC tokens. 

94 

95 :param mac_algorithm: Hashing algorithm for MAC tokens. 

96 

97 :param token: A dict of token attributes such as ``access_token``, 

98 ``token_type`` and ``expires_at``. 

99 

100 :param scope: A list of default scopes to request authorization for. 

101 

102 :param state: A CSRF protection string used during authorization. 

103 

104 :param redirect_url: The redirection endpoint on the client side to which 

105 the user returns after authorization. 

106 

107 :param state_generator: A no argument state generation callable. Defaults 

108 to :py:meth:`oauthlib.common.generate_token`. 

109 

110 :param code_verifier: PKCE parameter. A cryptographically random string that is used to correlate the 

111 authorization request to the token request. 

112 

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. 

115 

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 """ 

119 

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) 

139 

140 @property 

141 def token_types(self): 

142 """Supported token types and their respective methods 

143 

144 Additional tokens can be supported by extending this dictionary. 

145 

146 The Bearer token spec is stable and safe to use. 

147 

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 } 

155 

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.") 

159 

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.") 

163 

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.") 

167 

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. 

171 

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. 

177 

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: 

181 

182 .. code-block:: http 

183 

184 GET /resource/1 HTTP/1.1 

185 Host: example.com 

186 Authorization: Bearer mF_9.B5f-4.1JqM 

187 

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: 

191 

192 .. code-block:: http 

193 

194 GET /resource/1 HTTP/1.1 

195 Host: example.com 

196 Authorization: MAC id="h480djs93hd8", 

197 nonce="274312:dj83hs9s", 

198 mac="kDZvddkndxvhGRXZhvuDjEWhGeE=" 

199 

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() 

205 

206 token_placement = token_placement or self.default_token_placement 

207 

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) 

212 

213 if not (self.access_token or self.token.get('access_token')): 

214 raise ValueError("Missing access token.") 

215 

216 if self._expires_at and self._expires_at < time.time(): 

217 raise TokenExpiredError() 

218 

219 return case_insensitive_token_types[self.token_type.lower()](uri, http_method, body, 

220 headers, token_placement, **kwargs) 

221 

222 def prepare_authorization_request(self, authorization_url, state=None, 

223 redirect_url=None, scope=None, **kwargs): 

224 """Prepare the authorization request. 

225 

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. 

229 

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() 

250 

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, '' 

259 

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. 

263 

264 Note that these requests usually require client authentication, either 

265 by including client_id or a set of provider specific authentication 

266 credentials. 

267 

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() 

283 

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) 

291 

292 return token_url, FORM_ENC_HEADERS, body 

293 

294 def prepare_refresh_token_request(self, token_url, refresh_token=None, 

295 body='', scope=None, **kwargs): 

296 """Prepare an access token refresh request. 

297 

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. 

302 

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() 

316 

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 

322 

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. 

326 

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). 

337 

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. 

340 

341 An example of a revocation request 

342 

343 .. code-block:: http 

344 

345 POST /revoke HTTP/1.1 

346 Host: server.example.com 

347 Content-Type: application/x-www-form-urlencoded 

348 Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW 

349 

350 token=45ghiukldjahdnhzdauz&token_type_hint=refresh_token 

351 

352 An example of a jsonp revocation request 

353 

354 .. code-block:: http 

355 

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 

360 

361 and an error response 

362 

363 .. code-block:: javascript 

364 

365 package.myCallback({"error":"unsupported_token_type"}); 

366 

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() 

373 

374 return prepare_token_revocation_request(revocation_url, token, 

375 token_type_hint=token_type_hint, body=body, callback=callback, 

376 **kwargs) 

377 

378 def parse_request_body_response(self, body, scope=None, **kwargs): 

379 """Parse the JSON response body. 

380 

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`_. 

386 

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. 

393 

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. 

398 

399 A successful response should always contain 

400 

401 **access_token** 

402 The access token issued by the authorization server. Often 

403 a random string. 

404 

405 **token_type** 

406 The type of the token issued as described in `Section 7.1`_. 

407 Commonly ``Bearer``. 

408 

409 While it is not mandated it is recommended that the provider include 

410 

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. 

417 

418 **scope** 

419 Providers may supply this in all responses but are required to only 

420 if it has changed since the authorization request. 

421 

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 

430 

431 def prepare_refresh_body(self, body='', refresh_token=None, scope=None, **kwargs): 

432 """Prepare an access token request, using a refresh token. 

433 

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: 

438 

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) 

451 

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) 

457 

458 elif token_placement == URI_QUERY: 

459 uri = tokens.prepare_bearer_uri(self.access_token, uri) 

460 

461 elif token_placement == BODY: 

462 body = tokens.prepare_bearer_body(self.access_token, body) 

463 

464 else: 

465 raise ValueError("Invalid token placement.") 

466 return uri, headers, body 

467 

468 def create_code_verifier(self, length): 

469 """Create PKCE **code_verifier** used in computing **code_challenge**.  

470 See `RFC7636 Section 4.1`_ 

471 

472 :param length: REQUIRED. The length of the code_verifier. 

473 

474 The client first creates a code verifier, "code_verifier", for each 

475 OAuth 2.0 [RFC6749] Authorization Request, in the following manner: 

476 

477 .. code-block:: text 

478 

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. 

483 

484 .. _`RFC7636 Section 4.1`: https://tools.ietf.org/html/rfc7636#section-4.1 

485 """ 

486 code_verifier = None 

487 

488 if not length >= 43: 

489 raise ValueError("Length must be greater than or equal to 43") 

490 

491 if not length <= 128: 

492 raise ValueError("Length must be less than or equal to 128") 

493 

494 allowed_characters = re.compile('^[A-Zaa-z0-9-._~]') 

495 code_verifier = secrets.token_urlsafe(length) 

496 

497 if not re.search(allowed_characters, code_verifier): 

498 raise ValueError("code_verifier contains invalid characters") 

499 

500 self.code_verifier = code_verifier 

501 

502 return code_verifier 

503 

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`_ 

507 

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`. 

510 

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:: 

514 

515 plain 

516 code_challenge = code_verifier 

517 S256 

518 code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) 

519 

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`. 

525 

526 The plain transformation is for compatibility with existing 

527 deployments and for constrained environments that can't use the S256 transformation. 

528 

529 .. _`RFC7636 Section 4.2`: https://tools.ietf.org/html/rfc7636#section-4.2 

530 """ 

531 code_challenge = None 

532 

533 if code_verifier == None: 

534 raise ValueError("Invalid code_verifier") 

535 

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 

541 

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 

550 

551 return code_challenge 

552 

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. 

556 

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.") 

561 

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 

566 

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) 

571 

572 def populate_code_attributes(self, response): 

573 """Add attributes from an auth code response to self.""" 

574 

575 if 'code' in response: 

576 self.code = response.get('code') 

577 

578 def populate_token_attributes(self, response): 

579 """Add attributes from a token exchange response to self.""" 

580 

581 if 'access_token' in response: 

582 self.access_token = response.get('access_token') 

583 

584 if 'refresh_token' in response: 

585 self.refresh_token = response.get('refresh_token') 

586 

587 if 'token_type' in response: 

588 self.token_type = response.get('token_type') 

589 

590 if 'expires_in' in response: 

591 self.expires_in = response.get('expires_in') 

592 self._expires_at = time.time() + int(self.expires_in) 

593 

594 if 'expires_at' in response: 

595 try: 

596 self._expires_at = int(response.get('expires_at')) 

597 except: 

598 self._expires_at = None 

599 

600 if 'mac_key' in response: 

601 self.mac_key = response.get('mac_key') 

602 

603 if 'mac_algorithm' in response: 

604 self.mac_algorithm = response.get('mac_algorithm')