Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/oauthlib/oauth2/rfc6749/clients/base.py: 23%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

158 statements  

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 time 

12import warnings 

13 

14from oauthlib.common import UNICODE_ASCII_CHARACTER_SET, generate_token 

15from oauthlib.oauth2.rfc6749 import tokens 

16from oauthlib.oauth2.rfc6749.errors import ( 

17 InsecureTransportError, TokenExpiredError, 

18) 

19from oauthlib.oauth2.rfc6749.parameters import ( 

20 parse_expires, 

21 parse_token_response, prepare_token_request, 

22 prepare_token_revocation_request, 

23) 

24from oauthlib.oauth2.rfc6749.utils import is_secure_transport 

25 

26AUTH_HEADER = 'auth_header' 

27URI_QUERY = 'query' 

28BODY = 'body' 

29 

30FORM_ENC_HEADERS = { 

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

32} 

33 

34 

35class Client: 

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

37 

38 This class also acts as a generic interface providing methods common to all 

39 client types such as ``prepare_authorization_request`` and 

40 ``prepare_token_revocation_request``. The ``prepare_x_request`` methods are 

41 the recommended way of interacting with clients (as opposed to the abstract 

42 prepare uri/body/etc methods). They are recommended over the older set 

43 because they are easier to use (more consistent) and add a few additional 

44 security checks, such as HTTPS and state checking. 

45 

46 Some of these methods require further implementation only provided by the 

47 specific purpose clients such as 

48 :py:class:`oauthlib.oauth2.MobileApplicationClient` and thus you should always 

49 seek to use the client class matching the OAuth workflow you need. For 

50 Python, this is usually :py:class:`oauthlib.oauth2.WebApplicationClient`. 

51 

52 """ 

53 refresh_token_key = 'refresh_token' 

54 

55 def __init__(self, client_id, 

56 default_token_placement=AUTH_HEADER, 

57 token_type='Bearer', 

58 access_token=None, 

59 refresh_token=None, 

60 mac_key=None, 

61 mac_algorithm=None, 

62 token=None, 

63 scope=None, 

64 state=None, 

65 redirect_url=None, 

66 state_generator=generate_token, 

67 code_verifier=None, 

68 code_challenge=None, 

69 code_challenge_method=None, 

70 **kwargs): 

71 """Initialize a client with commonly used attributes. 

72 

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

74 registration. 

75 

76 :param default_token_placement: Tokens can be supplied in the Authorization 

77 header (default), the URL query component (``query``) or the request 

78 body (``body``). 

79 

80 :param token_type: OAuth 2 token type. Defaults to Bearer. Change this 

81 if you specify the ``access_token`` parameter and know it is of a 

82 different token type, such as a MAC, JWT or SAML token. Can 

83 also be supplied as ``token_type`` inside the ``token`` dict parameter. 

84 

85 :param access_token: An access token (string) used to authenticate 

86 requests to protected resources. Can also be supplied inside the 

87 ``token`` dict parameter. 

88 

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

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

91 

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

93 

94 :param mac_algorithm: Hashing algorithm for MAC tokens. 

95 

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

97 ``token_type`` and ``expires_at``. 

98 

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

100 

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

102 

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

104 the user returns after authorization. 

105 

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

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

108 

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

110 authorization request to the token request. 

111 

112 :param code_challenge: PKCE parameter. A challenge derived from the code verifier that is sent in the 

113 authorization request, to be verified against later. 

114 

115 :param code_challenge_method: PKCE parameter. A method that was used to derive code challenge. 

116 Defaults to "plain" if not present in the request. 

117 """ 

118 

119 self.client_id = client_id 

120 self.default_token_placement = default_token_placement 

121 self.token_type = token_type 

122 self.access_token = access_token 

123 self.refresh_token = refresh_token 

124 self.mac_key = mac_key 

125 self.mac_algorithm = mac_algorithm 

126 self.token = token or {} 

127 self.scope = scope 

128 self.state_generator = state_generator 

129 self.state = state 

130 self.redirect_url = redirect_url 

131 self.code_verifier = code_verifier 

132 self.code_challenge = code_challenge 

133 self.code_challenge_method = code_challenge_method 

134 self.code = None 

135 self.expires_in = None 

136 self._expires_at = None 

137 self.populate_token_attributes(self.token) 

138 

139 @property 

140 def token_types(self): 

141 """Supported token types and their respective methods 

142 

143 Additional tokens can be supported by extending this dictionary. 

144 

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

146 

147 The MAC token spec is not yet stable and support for MAC tokens 

148 is experimental and currently matching version 00 of the spec. 

149 """ 

150 return { 

151 'Bearer': self._add_bearer_token, 

152 'MAC': self._add_mac_token 

153 } 

154 

155 def prepare_request_uri(self, *args, **kwargs): 

156 """Abstract method used to create request URIs.""" 

157 raise NotImplementedError("Must be implemented by inheriting classes.") 

158 

159 def prepare_request_body(self, *args, **kwargs): 

160 """Abstract method used to create request bodies.""" 

161 raise NotImplementedError("Must be implemented by inheriting classes.") 

162 

163 def parse_request_uri_response(self, *args, **kwargs): 

164 """Abstract method used to parse redirection responses.""" 

165 raise NotImplementedError("Must be implemented by inheriting classes.") 

166 

167 def add_token(self, uri, http_method='GET', body=None, headers=None, 

168 token_placement=None, **kwargs): 

169 """Add token to the request uri, body or authorization header. 

170 

171 The access token type provides the client with the information 

172 required to successfully utilize the access token to make a protected 

173 resource request (along with type-specific attributes). The client 

174 MUST NOT use an access token if it does not understand the token 

175 type. 

176 

177 For example, the "bearer" token type defined in 

178 [`I-D.ietf-oauth-v2-bearer`_] is utilized by simply including the access 

179 token string in the request: 

180 

181 .. code-block:: http 

182 

183 GET /resource/1 HTTP/1.1 

184 Host: example.com 

185 Authorization: Bearer mF_9.B5f-4.1JqM 

186 

187 while the "mac" token type defined in [`I-D.ietf-oauth-v2-http-mac`_] is 

188 utilized by issuing a MAC key together with the access token which is 

189 used to sign certain components of the HTTP requests: 

190 

191 .. code-block:: http 

192 

193 GET /resource/1 HTTP/1.1 

194 Host: example.com 

195 Authorization: MAC id="h480djs93hd8", 

196 nonce="274312:dj83hs9s", 

197 mac="kDZvddkndxvhGRXZhvuDjEWhGeE=" 

198 

199 .. _`I-D.ietf-oauth-v2-bearer`: https://tools.ietf.org/html/rfc6749#section-12.2 

200 .. _`I-D.ietf-oauth-v2-http-mac`: https://tools.ietf.org/html/rfc6749#section-12.2 

201 """ 

202 if not is_secure_transport(uri): 

203 raise InsecureTransportError() 

204 

205 token_placement = token_placement or self.default_token_placement 

206 

207 case_insensitive_token_types = { 

208 k.lower(): v for k, v in self.token_types.items()} 

209 if self.token_type.lower() not in case_insensitive_token_types: 

210 raise ValueError("Unsupported token type: %s" % self.token_type) 

211 

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

213 raise ValueError("Missing access token.") 

214 

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

216 raise TokenExpiredError() 

217 

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

219 headers, token_placement, **kwargs) 

220 

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

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

223 """Prepare the authorization request. 

224 

225 This is the first step in many OAuth flows in which the user is 

226 redirected to a certain authorization URL. This method adds 

227 required parameters to the authorization URL. 

228 

229 :param authorization_url: Provider authorization endpoint URL. 

230 :param state: CSRF protection string. Will be automatically created if 

231 not provided. The generated state is available via the ``state`` 

232 attribute. Clients should verify that the state is unchanged and 

233 present in the authorization response. This verification is done 

234 automatically if using the ``authorization_response`` parameter 

235 with ``prepare_token_request``. 

236 :param redirect_url: Redirect URL to which the user will be returned 

237 after authorization. Must be provided unless previously setup with 

238 the provider. If provided then it must also be provided in the 

239 token request. 

240 :param scope: List of scopes to request. Must be equal to 

241 or a subset of the scopes granted when obtaining the refresh 

242 token. If none is provided, the ones provided in the constructor are 

243 used. 

244 :param kwargs: Additional parameters to included in the request. 

245 :returns: The prepared request tuple with (url, headers, body). 

246 """ 

247 if not is_secure_transport(authorization_url): 

248 raise InsecureTransportError() 

249 

250 self.state = state or self.state_generator() 

251 self.redirect_url = redirect_url or self.redirect_url 

252 # do not assign scope to self automatically anymore 

253 scope = self.scope if scope is None else scope 

254 auth_url = self.prepare_request_uri( 

255 authorization_url, redirect_uri=self.redirect_url, 

256 scope=scope, state=self.state, **kwargs) 

257 return auth_url, FORM_ENC_HEADERS, '' 

258 

259 def prepare_token_request(self, token_url, authorization_response=None, 

260 redirect_url=None, state=None, body='', **kwargs): 

261 """Prepare a token creation request. 

262 

263 Note that these requests usually require client authentication, either 

264 by including client_id or a set of provider specific authentication 

265 credentials. 

266 

267 :param token_url: Provider token creation endpoint URL. 

268 :param authorization_response: The full redirection URL string, i.e. 

269 the location to which the user was redirected after successful 

270 authorization. Used to mine credentials needed to obtain a token 

271 in this step, such as authorization code. 

272 :param redirect_url: The redirect_url supplied with the authorization 

273 request (if there was one). 

274 :param state: 

275 :param body: Existing request body (URL encoded string) to embed parameters 

276 into. This may contain extra parameters. Default ''. 

277 :param kwargs: Additional parameters to included in the request. 

278 :returns: The prepared request tuple with (url, headers, body). 

279 """ 

280 if not is_secure_transport(token_url): 

281 raise InsecureTransportError() 

282 

283 state = state or self.state 

284 if authorization_response: 

285 self.parse_request_uri_response( 

286 authorization_response, state=state) 

287 self.redirect_url = redirect_url or self.redirect_url 

288 body = self.prepare_request_body(body=body, 

289 redirect_uri=self.redirect_url, **kwargs) 

290 

291 return token_url, FORM_ENC_HEADERS, body 

292 

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

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

295 """Prepare an access token refresh request. 

296 

297 Expired access tokens can be replaced by new access tokens without 

298 going through the OAuth dance if the client obtained a refresh token. 

299 This refresh token and authentication credentials can be used to 

300 obtain a new access token, and possibly a new refresh token. 

301 

302 :param token_url: Provider token refresh endpoint URL. 

303 :param refresh_token: Refresh token string. 

304 :param body: Existing request body (URL encoded string) to embed parameters 

305 into. This may contain extra parameters. Default ''. 

306 :param scope: List of scopes to request. Must be equal to 

307 or a subset of the scopes granted when obtaining the refresh 

308 token. If none is provided, the ones provided in the constructor are 

309 used. 

310 :param kwargs: Additional parameters to included in the request. 

311 :returns: The prepared request tuple with (url, headers, body). 

312 """ 

313 if not is_secure_transport(token_url): 

314 raise InsecureTransportError() 

315 

316 # do not assign scope to self automatically anymore 

317 scope = self.scope if scope is None else scope 

318 body = self.prepare_refresh_body(body=body, 

319 refresh_token=refresh_token, scope=scope, **kwargs) 

320 return token_url, FORM_ENC_HEADERS, body 

321 

322 def prepare_token_revocation_request(self, revocation_url, token, 

323 token_type_hint="access_token", body='', callback=None, **kwargs): 

324 """Prepare a token revocation request. 

325 

326 :param revocation_url: Provider token revocation endpoint URL. 

327 :param token: The access or refresh token to be revoked (string). 

328 :param token_type_hint: ``"access_token"`` (default) or 

329 ``"refresh_token"``. This is optional and if you wish to not pass it you 

330 must provide ``token_type_hint=None``. 

331 :param body: 

332 :param callback: A jsonp callback such as ``package.callback`` to be invoked 

333 upon receiving the response. Not that it should not include a () suffix. 

334 :param kwargs: Additional parameters to included in the request. 

335 :returns: The prepared request tuple with (url, headers, body). 

336 

337 Note that JSONP request may use GET requests as the parameters will 

338 be added to the request URL query as opposed to the request body. 

339 

340 An example of a revocation request 

341 

342 .. code-block:: http 

343 

344 POST /revoke HTTP/1.1 

345 Host: server.example.com 

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

347 Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW 

348 

349 token=45ghiukldjahdnhzdauz&token_type_hint=refresh_token 

350 

351 An example of a jsonp revocation request 

352 

353 .. code-block:: http 

354 

355 GET /revoke?token=agabcdefddddafdd&callback=package.myCallback HTTP/1.1 

356 Host: server.example.com 

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

358 Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW 

359 

360 and an error response 

361 

362 .. code-block:: javascript 

363 

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

365 

366 Note that these requests usually require client credentials, client_id in 

367 the case for public clients and provider specific authentication 

368 credentials for confidential clients. 

369 """ 

370 if not is_secure_transport(revocation_url): 

371 raise InsecureTransportError() 

372 

373 return prepare_token_revocation_request(revocation_url, token, 

374 token_type_hint=token_type_hint, body=body, callback=callback, 

375 **kwargs) 

376 

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

378 """Parse the JSON response body. 

379 

380 If the access token request is valid and authorized, the 

381 authorization server issues an access token as described in 

382 `Section 5.1`_. A refresh token SHOULD NOT be included. If the request 

383 failed client authentication or is invalid, the authorization server 

384 returns an error response as described in `Section 5.2`_. 

385 

386 :param body: The response body from the token request. 

387 :param scope: Scopes originally requested. If none is provided, the ones 

388 provided in the constructor are used. 

389 :return: Dictionary of token parameters. 

390 :raises: Warning if scope has changed. :py:class:`oauthlib.oauth2.errors.OAuth2Error` 

391 if response is invalid. 

392 

393 These response are json encoded and could easily be parsed without 

394 the assistance of OAuthLib. However, there are a few subtle issues 

395 to be aware of regarding the response which are helpfully addressed 

396 through the raising of various errors. 

397 

398 A successful response should always contain 

399 

400 **access_token** 

401 The access token issued by the authorization server. Often 

402 a random string. 

403 

404 **token_type** 

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

406 Commonly ``Bearer``. 

407 

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

409 

410 **expires_in** 

411 The lifetime in seconds of the access token. For 

412 example, the value "3600" denotes that the access token will 

413 expire in one hour from the time the response was generated. 

414 If omitted, the authorization server SHOULD provide the 

415 expiration time via other means or document the default value. 

416 

417 **scope** 

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

419 if it has changed since the authorization request. 

420 

421 .. _`Section 5.1`: https://tools.ietf.org/html/rfc6749#section-5.1 

422 .. _`Section 5.2`: https://tools.ietf.org/html/rfc6749#section-5.2 

423 .. _`Section 7.1`: https://tools.ietf.org/html/rfc6749#section-7.1 

424 """ 

425 scope = self.scope if scope is None else scope 

426 self.token = parse_token_response(body, scope=scope) 

427 self.populate_token_attributes(self.token) 

428 return self.token 

429 

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

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

432 

433 If the authorization server issued a refresh token to the client, the 

434 client makes a refresh request to the token endpoint by adding the 

435 following parameters using the `application/x-www-form-urlencoded` 

436 format in the HTTP request entity-body: 

437 

438 :param refresh_token: REQUIRED. The refresh token issued to the client. 

439 :param scope: OPTIONAL. The scope of the access request as described by 

440 Section 3.3. The requested scope MUST NOT include any scope 

441 not originally granted by the resource owner, and if omitted is 

442 treated as equal to the scope originally granted by the 

443 resource owner. Note that if none is provided, the ones provided 

444 in the constructor are used if any. 

445 """ 

446 refresh_token = refresh_token or self.refresh_token 

447 scope = self.scope if scope is None else scope 

448 return prepare_token_request(self.refresh_token_key, body=body, scope=scope, 

449 refresh_token=refresh_token, **kwargs) 

450 

451 def _add_bearer_token(self, uri, http_method='GET', body=None, 

452 headers=None, token_placement=None): 

453 """Add a bearer token to the request uri, body or authorization header.""" 

454 if token_placement == AUTH_HEADER: 

455 headers = tokens.prepare_bearer_headers(self.access_token, headers) 

456 

457 elif token_placement == URI_QUERY: 

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

459 

460 elif token_placement == BODY: 

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

462 

463 else: 

464 raise ValueError("Invalid token placement.") 

465 return uri, headers, body 

466 

467 def create_code_verifier(self, length): 

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

469 See `RFC7636 Section 4.1`_ 

470 

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

472 

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

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

475 

476 .. code-block:: text 

477 

478 code_verifier = high-entropy cryptographic random STRING using the 

479 unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" 

480 from Section 2.3 of [RFC3986], with a minimum length of 43 characters 

481 and a maximum length of 128 characters. 

482 

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

484 """ 

485 code_verifier = None 

486 

487 if not length >= 43: 

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

489 

490 if not length <= 128: 

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

492 

493 code_verifier = generate_token(length, UNICODE_ASCII_CHARACTER_SET + "-._~") 

494 

495 self.code_verifier = code_verifier 

496 

497 return code_verifier 

498 

499 def create_code_challenge(self, code_verifier, code_challenge_method=None): 

500 """Create PKCE **code_challenge** derived from the **code_verifier**. 

501 See `RFC7636 Section 4.2`_ 

502 

503 :param code_verifier: REQUIRED. The **code_verifier** generated from `create_code_verifier()`. 

504 :param code_challenge_method: OPTIONAL. The method used to derive the **code_challenge**. Acceptable values include `S256`. DEFAULT is `plain`. 

505 

506 The client then creates a code challenge derived from the code 

507 verifier by using one of the following transformations on the code 

508 verifier:: 

509 

510 plain 

511 code_challenge = code_verifier 

512 S256 

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

514 

515 If the client is capable of using `S256`, it MUST use `S256`, as 

516 `S256` is Mandatory To Implement (MTI) on the server. Clients are 

517 permitted to use `plain` only if they cannot support `S256` for some 

518 technical reason and know via out-of-band configuration that the 

519 server supports `plain`. 

520 

521 The plain transformation is for compatibility with existing 

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

523 

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

525 """ 

526 code_challenge = None 

527 

528 if code_verifier is None: 

529 raise ValueError("Invalid code_verifier") 

530 

531 if code_challenge_method is None: 

532 code_challenge_method = "plain" 

533 self.code_challenge_method = code_challenge_method 

534 code_challenge = code_verifier 

535 self.code_challenge = code_challenge 

536 

537 if code_challenge_method == "S256": 

538 h = hashlib.sha256() 

539 h.update(code_verifier.encode(encoding='ascii')) 

540 sha256_val = h.digest() 

541 code_challenge = bytes.decode(base64.urlsafe_b64encode(sha256_val)) 

542 # replace '+' with '-', '/' with '_', and remove trailing '=' 

543 code_challenge = code_challenge.replace("+", "-").replace("/", "_").replace("=", "") 

544 self.code_challenge = code_challenge 

545 

546 return code_challenge 

547 

548 def _add_mac_token(self, uri, http_method='GET', body=None, 

549 headers=None, token_placement=AUTH_HEADER, ext=None, **kwargs): 

550 """Add a MAC token to the request authorization header. 

551 

552 Warning: MAC token support is experimental as the spec is not yet stable. 

553 """ 

554 if token_placement != AUTH_HEADER: 

555 raise ValueError("Invalid token placement.") 

556 

557 headers = tokens.prepare_mac_header(self.access_token, uri, 

558 self.mac_key, http_method, headers=headers, body=body, ext=ext, 

559 hash_algorithm=self.mac_algorithm, **kwargs) 

560 return uri, headers, body 

561 

562 def _populate_attributes(self, response): 

563 warnings.warn("Please switch to the public method " 

564 "populate_token_attributes.", DeprecationWarning) 

565 return self.populate_token_attributes(response) 

566 

567 def populate_code_attributes(self, response): 

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

569 

570 if 'code' in response: 

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

572 

573 def populate_token_attributes(self, response): 

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

575 

576 if 'access_token' in response: 

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

578 

579 if 'refresh_token' in response: 

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

581 

582 if 'token_type' in response: 

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

584 

585 vin, vat, v_at = parse_expires(response) 

586 if vin: 

587 self.expires_in = vin 

588 if vat: 

589 self.expires_at = vat 

590 if v_at: 

591 self._expires_at = v_at 

592 

593 if 'mac_key' in response: 

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

595 

596 if 'mac_algorithm' in response: 

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