Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/msal/oauth2cli/oauth2.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

268 statements  

1"""This OAuth2 client implementation aims to be spec-compliant, and generic.""" 

2# OAuth2 spec https://tools.ietf.org/html/rfc6749 

3 

4import json 

5try: 

6 from urllib.parse import urlencode, parse_qs, quote_plus, urlparse, urlunparse 

7except ImportError: 

8 from urlparse import parse_qs, urlparse, urlunparse 

9 from urllib import urlencode, quote_plus 

10import logging 

11import warnings 

12import time 

13import base64 

14import sys 

15import functools 

16import random 

17import string 

18import hashlib 

19 

20from .authcode import AuthCodeReceiver as _AuthCodeReceiver 

21 

22try: 

23 PermissionError # Available in Python 3 

24except: 

25 from socket import error as PermissionError # Workaround for Python 2 

26 

27 

28string_types = (str,) if sys.version_info[0] >= 3 else (basestring, ) 

29 

30 

31class BrowserInteractionTimeoutError(RuntimeError): 

32 pass 

33 

34class BaseClient(object): 

35 # This low-level interface works. Yet you'll find its sub-class 

36 # more friendly to remind you what parameters are needed in each scenario. 

37 # More on Client Types at https://tools.ietf.org/html/rfc6749#section-2.1 

38 

39 @staticmethod 

40 def encode_saml_assertion(assertion): 

41 return base64.urlsafe_b64encode(assertion).rstrip(b'=') # Per RFC 7522 

42 

43 CLIENT_ASSERTION_TYPE_JWT = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" 

44 CLIENT_ASSERTION_TYPE_SAML2 = "urn:ietf:params:oauth:client-assertion-type:saml2-bearer" 

45 client_assertion_encoders = {CLIENT_ASSERTION_TYPE_SAML2: encode_saml_assertion} 

46 

47 @property 

48 def session(self): 

49 warnings.warn("Will be gone in next major release", DeprecationWarning) 

50 return self._http_client 

51 

52 @session.setter 

53 def session(self, value): 

54 warnings.warn("Will be gone in next major release", DeprecationWarning) 

55 self._http_client = value 

56 

57 

58 def __init__( 

59 self, 

60 server_configuration, # type: dict 

61 client_id, # type: str 

62 http_client=None, # We insert it here to match the upcoming async API 

63 client_secret=None, # type: Optional[str] 

64 client_assertion=None, # type: Union[bytes, callable, None] 

65 client_assertion_type=None, # type: Optional[str] 

66 default_headers=None, # type: Optional[dict] 

67 default_body=None, # type: Optional[dict] 

68 verify=None, # type: Union[str, True, False, None] 

69 proxies=None, # type: Optional[dict] 

70 timeout=None, # type: Union[tuple, float, None] 

71 ): 

72 """Initialize a client object to talk all the OAuth2 grants to the server. 

73 

74 Args: 

75 server_configuration (dict): 

76 It contains the configuration (i.e. metadata) of the auth server. 

77 The actual content typically contains keys like 

78 "authorization_endpoint", "token_endpoint", etc.. 

79 Based on RFC 8414 (https://tools.ietf.org/html/rfc8414), 

80 you can probably fetch it online from either 

81 https://example.com/.../.well-known/oauth-authorization-server 

82 or 

83 https://example.com/.../.well-known/openid-configuration 

84 client_id (str): The client's id, issued by the authorization server 

85 

86 http_client (http.HttpClient): 

87 Your implementation of abstract class :class:`http.HttpClient`. 

88 Defaults to a requests session instance. 

89 

90 There is no session-wide `timeout` parameter defined here. 

91 Timeout behavior is determined by the actual http client you use. 

92 If you happen to use Requests, it disallows session-wide timeout 

93 (https://github.com/psf/requests/issues/3341). The workaround is: 

94 

95 s = requests.Session() 

96 s.request = functools.partial(s.request, timeout=3) 

97 

98 and then feed that patched session instance to this class. 

99 

100 client_secret (str): Triggers HTTP AUTH for Confidential Client 

101 client_assertion (bytes, callable): 

102 The client assertion to authenticate this client, per RFC 7521. 

103 It can be a raw SAML2 assertion (we will base64 encode it for you), 

104 or a raw JWT assertion in bytes (which we will relay to http layer). 

105 It can also be a callable (recommended), 

106 so that we will do lazy creation of an assertion. 

107 client_assertion_type (str): 

108 The type of your :attr:`client_assertion` parameter. 

109 It is typically the value of :attr:`CLIENT_ASSERTION_TYPE_SAML2` or 

110 :attr:`CLIENT_ASSERTION_TYPE_JWT`, the only two defined in RFC 7521. 

111 default_headers (dict): 

112 A dict to be sent in each request header. 

113 It is not required by OAuth2 specs, but you may use it for telemetry. 

114 default_body (dict): 

115 A dict to be sent in each token request body. For example, 

116 you could choose to set this as {"client_secret": "your secret"} 

117 if your authorization server wants it to be in the request body 

118 (rather than in the request header). 

119 

120 verify (boolean): 

121 It will be passed to the 

122 `verify parameter in the underlying requests library 

123 <http://docs.python-requests.org/en/v2.9.1/user/advanced/#ssl-cert-verification>`_. 

124 When leaving it with default value (None), we will use True instead. 

125 

126 This does not apply if you have chosen to pass your own Http client. 

127 

128 proxies (dict): 

129 It will be passed to the 

130 `proxies parameter in the underlying requests library 

131 <http://docs.python-requests.org/en/v2.9.1/user/advanced/#proxies>`_. 

132 

133 This does not apply if you have chosen to pass your own Http client. 

134 

135 timeout (object): 

136 It will be passed to the 

137 `timeout parameter in the underlying requests library 

138 <http://docs.python-requests.org/en/v2.9.1/user/advanced/#timeouts>`_. 

139 

140 This does not apply if you have chosen to pass your own Http client. 

141 

142 """ 

143 if not server_configuration: 

144 raise ValueError("Missing input parameter server_configuration") 

145 # Generally we should have client_id, but we tolerate its absence 

146 self.configuration = server_configuration 

147 self.client_id = client_id 

148 self.client_secret = client_secret 

149 self.client_assertion = client_assertion 

150 self.default_headers = default_headers or {} 

151 self.default_body = default_body or {} 

152 if client_assertion_type is not None: 

153 self.default_body["client_assertion_type"] = client_assertion_type 

154 self.logger = logging.getLogger(__name__) 

155 if http_client: 

156 if verify is not None or proxies is not None or timeout is not None: 

157 raise ValueError( 

158 "verify, proxies, or timeout is not allowed " 

159 "when http_client is in use") 

160 self._http_client = http_client 

161 else: 

162 import requests # Lazy loading 

163 

164 self._http_client = requests.Session() 

165 self._http_client.verify = True if verify is None else verify 

166 self._http_client.proxies = proxies 

167 self._http_client.request = functools.partial( 

168 # A workaround for requests not supporting session-wide timeout 

169 self._http_client.request, timeout=timeout) 

170 

171 def _build_auth_request_params(self, response_type, **kwargs): 

172 # response_type is a string defined in 

173 # https://tools.ietf.org/html/rfc6749#section-3.1.1 

174 # or it can be a space-delimited string as defined in 

175 # https://tools.ietf.org/html/rfc6749#section-8.4 

176 response_type = self._stringify(response_type) 

177 

178 params = {'client_id': self.client_id, 'response_type': response_type} 

179 params.update(kwargs) # Note: None values will override params 

180 params = {k: v for k, v in params.items() if v is not None} # clean up 

181 if params.get('scope'): 

182 params['scope'] = self._stringify(params['scope']) 

183 return params # A dict suitable to be used in http request 

184 

185 def _obtain_token( # The verb "obtain" is influenced by OAUTH2 RFC 6749 

186 self, grant_type, 

187 params=None, # a dict to be sent as query string to the endpoint 

188 data=None, # All relevant data, which will go into the http body 

189 headers=None, # a dict to be sent as request headers 

190 post=None, # A callable to replace requests.post(), for testing. 

191 # Such as: lambda url, **kwargs: 

192 # Mock(status_code=200, text='{}') 

193 **kwargs # Relay all extra parameters to underlying requests 

194 ): # Returns the json object came from the OAUTH2 response 

195 _data = {'client_id': self.client_id, 'grant_type': grant_type} 

196 

197 if self.default_body.get("client_assertion_type") and self.client_assertion: 

198 # See https://tools.ietf.org/html/rfc7521#section-4.2 

199 encoder = self.client_assertion_encoders.get( 

200 self.default_body["client_assertion_type"], lambda a: a) 

201 _data["client_assertion"] = encoder( 

202 self.client_assertion() # Do lazy on-the-fly computation 

203 if callable(self.client_assertion) else self.client_assertion 

204 ) # The type is bytes, which is preferable. See also: 

205 # https://github.com/psf/requests/issues/4503#issuecomment-455001070 

206 

207 _data.update(self.default_body) # It may contain authen parameters 

208 _data.update(data or {}) # So the content in data param prevails 

209 _data = {k: v for k, v in _data.items() if v} # Clean up None values 

210 

211 if _data.get('scope'): 

212 _data['scope'] = self._stringify(_data['scope']) 

213 

214 _headers = {'Accept': 'application/json'} 

215 _headers.update(self.default_headers) 

216 _headers.update(headers or {}) 

217 

218 # Quoted from https://tools.ietf.org/html/rfc6749#section-2.3.1 

219 # Clients in possession of a client password MAY use the HTTP Basic 

220 # authentication. 

221 # Alternatively, (but NOT RECOMMENDED,) 

222 # the authorization server MAY support including the 

223 # client credentials in the request-body using the following 

224 # parameters: client_id, client_secret. 

225 if self.client_secret and self.client_id: 

226 _headers["Authorization"] = "Basic " + base64.b64encode("{}:{}".format( 

227 # Per https://tools.ietf.org/html/rfc6749#section-2.3.1 

228 # client_id and client_secret needs to be encoded by 

229 # "application/x-www-form-urlencoded" 

230 # https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1 

231 # BEFORE they are fed into HTTP Basic Authentication 

232 quote_plus(self.client_id), quote_plus(self.client_secret) 

233 ).encode("ascii")).decode("ascii") 

234 

235 if "token_endpoint" not in self.configuration: 

236 raise ValueError("token_endpoint not found in configuration") 

237 resp = (post or self._http_client.post)( 

238 self.configuration["token_endpoint"], 

239 headers=_headers, params=params, data=_data, 

240 **kwargs) 

241 if resp.status_code >= 500: 

242 resp.raise_for_status() # TODO: Will probably retry here 

243 try: 

244 # The spec (https://tools.ietf.org/html/rfc6749#section-5.2) says 

245 # even an error response will be a valid json structure, 

246 # so we simply return it here, without needing to invent an exception. 

247 return json.loads(resp.text) 

248 except ValueError: 

249 self.logger.exception( 

250 "Token response is not in json format: %s", resp.text) 

251 raise 

252 

253 def obtain_token_by_refresh_token(self, refresh_token, scope=None, **kwargs): 

254 # type: (str, Union[str, list, set, tuple]) -> dict 

255 """Obtain an access token via a refresh token. 

256 

257 :param refresh_token: The refresh token issued to the client 

258 :param scope: If omitted, is treated as equal to the scope originally 

259 granted by the resource owner, 

260 according to https://tools.ietf.org/html/rfc6749#section-6 

261 """ 

262 assert isinstance(refresh_token, string_types) 

263 data = kwargs.pop('data', {}) 

264 data.update(refresh_token=refresh_token, scope=scope) 

265 return self._obtain_token("refresh_token", data=data, **kwargs) 

266 

267 def _stringify(self, sequence): 

268 if isinstance(sequence, (list, set, tuple)): 

269 return ' '.join(sorted(sequence)) # normalizing it, ascendingly 

270 return sequence # as-is 

271 

272 

273def _scope_set(scope): 

274 assert scope is None or isinstance(scope, (list, set, tuple)) 

275 return set(scope) if scope else set([]) 

276 

277 

278def _generate_pkce_code_verifier(length=43): 

279 assert 43 <= length <= 128 

280 verifier = "".join( # https://tools.ietf.org/html/rfc7636#section-4.1 

281 random.sample(string.ascii_letters + string.digits + "-._~", length)) 

282 code_challenge = ( 

283 # https://tools.ietf.org/html/rfc7636#section-4.2 

284 base64.urlsafe_b64encode(hashlib.sha256(verifier.encode("ascii")).digest()) 

285 .rstrip(b"=")) # Required by https://tools.ietf.org/html/rfc7636#section-3 

286 return { 

287 "code_verifier": verifier, 

288 "transformation": "S256", # In Python, sha256 is always available 

289 "code_challenge": code_challenge, 

290 } 

291 

292 

293class Client(BaseClient): # We choose to implement all 4 grants in 1 class 

294 """This is the main API for oauth2 client. 

295 

296 Its methods define and document parameters mentioned in OAUTH2 RFC 6749. 

297 """ 

298 DEVICE_FLOW = { # consts for device flow, that can be customized by sub-class 

299 "GRANT_TYPE": "urn:ietf:params:oauth:grant-type:device_code", 

300 "DEVICE_CODE": "device_code", 

301 } 

302 DEVICE_FLOW_RETRIABLE_ERRORS = ("authorization_pending", "slow_down") 

303 GRANT_TYPE_SAML2 = "urn:ietf:params:oauth:grant-type:saml2-bearer" # RFC7522 

304 GRANT_TYPE_JWT = "urn:ietf:params:oauth:grant-type:jwt-bearer" # RFC7523 

305 grant_assertion_encoders = {GRANT_TYPE_SAML2: BaseClient.encode_saml_assertion} 

306 

307 

308 def initiate_device_flow(self, scope=None, *, data=None, **kwargs): 

309 # type: (list, **dict) -> dict 

310 # The naming of this method is following the wording of this specs 

311 # https://tools.ietf.org/html/draft-ietf-oauth-device-flow-12#section-3.1 

312 """Initiate a device flow. 

313 

314 Returns the data defined in Device Flow specs. 

315 https://tools.ietf.org/html/draft-ietf-oauth-device-flow-12#section-3.2 

316 

317 You should then orchestrate the User Interaction as defined in here 

318 https://tools.ietf.org/html/draft-ietf-oauth-device-flow-12#section-3.3 

319 

320 And possibly here 

321 https://tools.ietf.org/html/draft-ietf-oauth-device-flow-12#section-3.3.1 

322 """ 

323 DAE = "device_authorization_endpoint" 

324 if not self.configuration.get(DAE): 

325 raise ValueError("You need to provide device authorization endpoint") 

326 _data = {"client_id": self.client_id, "scope": self._stringify(scope or [])} 

327 if isinstance(data, dict): 

328 _data.update(data) 

329 resp = self._http_client.post(self.configuration[DAE], 

330 data=_data, 

331 headers=dict(self.default_headers, **kwargs.pop("headers", {})), 

332 **kwargs) 

333 flow = json.loads(resp.text) 

334 flow["interval"] = int(flow.get("interval", 5)) # Some IdP returns string 

335 flow["expires_in"] = int(flow.get("expires_in", 1800)) 

336 flow["expires_at"] = time.time() + flow["expires_in"] # We invent this 

337 return flow 

338 

339 def _obtain_token_by_device_flow(self, flow, **kwargs): 

340 # type: (dict, **dict) -> dict 

341 # This method updates flow during each run. And it is non-blocking. 

342 now = time.time() 

343 skew = 1 

344 if flow.get("latest_attempt_at", 0) + flow.get("interval", 5) - skew > now: 

345 warnings.warn('Attempted too soon. Please do time.sleep(flow["interval"])') 

346 data = kwargs.pop("data", {}) 

347 data.update({ 

348 "client_id": self.client_id, 

349 self.DEVICE_FLOW["DEVICE_CODE"]: flow["device_code"], 

350 }) 

351 result = self._obtain_token( 

352 self.DEVICE_FLOW["GRANT_TYPE"], data=data, **kwargs) 

353 if result.get("error") == "slow_down": 

354 # Respecting https://tools.ietf.org/html/draft-ietf-oauth-device-flow-12#section-3.5 

355 flow["interval"] = flow.get("interval", 5) + 5 

356 flow["latest_attempt_at"] = now 

357 return result 

358 

359 def obtain_token_by_device_flow(self, 

360 flow, 

361 exit_condition=lambda flow: flow.get("expires_at", 0) < time.time(), 

362 **kwargs): 

363 # type: (dict, Callable) -> dict 

364 """Obtain token by a device flow object, with customizable polling effect. 

365 

366 Args: 

367 flow (dict): 

368 An object previously generated by initiate_device_flow(...). 

369 Its content WILL BE CHANGED by this method during each run. 

370 We share this object with you, so that you could implement 

371 your own loop, should you choose to do so. 

372 

373 exit_condition (Callable): 

374 This method implements a loop to provide polling effect. 

375 The loop's exit condition is calculated by this callback. 

376 

377 The default callback makes the loop run until the flow expires. 

378 Therefore, one of the ways to exit the polling early, 

379 is to change the flow["expires_at"] to a small number such as 0. 

380 

381 In case you are doing async programming, you may want to 

382 completely turn off the loop. You can do so by using a callback as: 

383 

384 exit_condition = lambda flow: True 

385 

386 to make the loop run only once, i.e. no polling, hence non-block. 

387 """ 

388 while True: 

389 result = self._obtain_token_by_device_flow(flow, **kwargs) 

390 if result.get("error") not in self.DEVICE_FLOW_RETRIABLE_ERRORS: 

391 return result 

392 for i in range(flow.get("interval", 5)): # Wait interval seconds 

393 if exit_condition(flow): 

394 return result 

395 time.sleep(1) # Shorten each round, to make exit more responsive 

396 

397 def _build_auth_request_uri( 

398 self, 

399 response_type, 

400 *, 

401 redirect_uri=None, scope=None, state=None, response_mode=None, 

402 **kwargs): 

403 if "authorization_endpoint" not in self.configuration: 

404 raise ValueError("authorization_endpoint not found in configuration") 

405 authorization_endpoint = self.configuration["authorization_endpoint"] 

406 if response_mode != 'form_post': 

407 warnings.warn( 

408 "response_mode='form_post' is recommended for better security. " 

409 "See https://www.rfc-editor.org/rfc/rfc9700.html#section-4.3.1" 

410 ) 

411 params = self._build_auth_request_params( 

412 response_type, redirect_uri=redirect_uri, scope=scope, state=state, 

413 response_mode=response_mode, 

414 **kwargs) 

415 sep = '&' if '?' in authorization_endpoint else '?' 

416 return "%s%s%s" % (authorization_endpoint, sep, urlencode(params)) 

417 

418 def build_auth_request_uri( 

419 self, 

420 response_type, redirect_uri=None, scope=None, state=None, **kwargs): 

421 # This method could be named build_authorization_request_uri() instead, 

422 # but then there would be a build_authentication_request_uri() in the OIDC 

423 # subclass doing almost the same thing. So we use a loose term "auth" here. 

424 """Generate an authorization uri to be visited by resource owner. 

425 

426 Parameters are the same as another method :func:`initiate_auth_code_flow()`, 

427 whose functionality is a superset of this method. 

428 

429 :return: The auth uri as a string. 

430 """ 

431 warnings.warn("Use initiate_auth_code_flow() instead. ", DeprecationWarning) 

432 return self._build_auth_request_uri( 

433 response_type, redirect_uri=redirect_uri, scope=scope, state=state, 

434 **kwargs) 

435 

436 def initiate_auth_code_flow( 

437 # The name is influenced by OIDC 

438 # https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth 

439 self, 

440 scope=None, redirect_uri=None, state=None, 

441 **kwargs): 

442 """Initiate an auth code flow. 

443 

444 Later when the response reaches your redirect_uri, 

445 you can use :func:`~obtain_token_by_auth_code_flow()` 

446 to complete the authentication/authorization. 

447 

448 This method also provides PKCE protection automatically. 

449 

450 :param list scope: 

451 It is a list of case-sensitive strings. 

452 Some ID provider can accept empty string to represent default scope. 

453 :param str redirect_uri: 

454 Optional. If not specified, server will use the pre-registered one. 

455 :param str state: 

456 An opaque value used by the client to 

457 maintain state between the request and callback. 

458 If absent, this library will automatically generate one internally. 

459 :param kwargs: Other parameters, typically defined in OpenID Connect. 

460 

461 :return: 

462 The auth code flow. It is a dict in this form:: 

463 

464 { 

465 "auth_uri": "https://...", // Guide user to visit this 

466 "state": "...", // You may choose to verify it by yourself, 

467 // or just let obtain_token_by_auth_code_flow() 

468 // do that for you. 

469 "...": "...", // Everything else are reserved and internal 

470 } 

471 

472 The caller is expected to:: 

473 

474 1. somehow store this content, typically inside the current session, 

475 2. guide the end user (i.e. resource owner) to visit that auth_uri, 

476 3. and then relay this dict and subsequent auth response to 

477 :func:`~obtain_token_by_auth_code_flow()`. 

478 """ 

479 response_type = kwargs.pop("response_type", "code") # Auth Code flow 

480 # Must be "code" when you are using Authorization Code Grant. 

481 # The "token" for Implicit Grant is not applicable thus not allowed. 

482 # It could theoretically be other 

483 # (possibly space-delimited) strings as registered extension value. 

484 # See https://tools.ietf.org/html/rfc6749#section-3.1.1 

485 if "token" in response_type: 

486 # Implicit grant would cause auth response coming back in #fragment, 

487 # but fragment won't reach a web service. 

488 raise ValueError('response_type="token ..." is not allowed') 

489 pkce = _generate_pkce_code_verifier() 

490 flow = { # These data are required by obtain_token_by_auth_code_flow() 

491 "state": state or "".join(random.sample(string.ascii_letters, 16)), 

492 "redirect_uri": redirect_uri, 

493 "scope": scope, 

494 } 

495 auth_uri = self._build_auth_request_uri( 

496 response_type, 

497 code_challenge=pkce["code_challenge"], 

498 code_challenge_method=pkce["transformation"], 

499 **dict(flow, **kwargs)) 

500 flow["auth_uri"] = auth_uri 

501 flow["code_verifier"] = pkce["code_verifier"] 

502 return flow 

503 

504 def obtain_token_by_auth_code_flow( 

505 self, 

506 auth_code_flow, 

507 auth_response, 

508 scope=None, 

509 **kwargs): 

510 """With the auth_response being redirected back, 

511 validate it against auth_code_flow, and then obtain tokens. 

512 

513 Internally, it implements PKCE to mitigate the auth code interception attack. 

514 

515 :param dict auth_code_flow: 

516 The same dict returned by :func:`~initiate_auth_code_flow()`. 

517 :param dict auth_response: 

518 A dict based on query string received from auth server. 

519 

520 :param scope: 

521 You don't usually need to use scope parameter here. 

522 Some Identity Provider allows you to provide 

523 a subset of what you specified during :func:`~initiate_auth_code_flow`. 

524 :type scope: collections.Iterable[str] 

525 

526 :return: 

527 * A dict containing "access_token" and/or "id_token", among others, 

528 depends on what scope was used. 

529 (See https://tools.ietf.org/html/rfc6749#section-5.1) 

530 * A dict containing "error", optionally "error_description", "error_uri". 

531 (It is either `this <https://tools.ietf.org/html/rfc6749#section-4.1.2.1>`_ 

532 or `that <https://tools.ietf.org/html/rfc6749#section-5.2>`_ 

533 * Most client-side data error would result in ValueError exception. 

534 So the usage pattern could be without any protocol details:: 

535 

536 def authorize(): # A controller in a web app 

537 try: 

538 result = client.obtain_token_by_auth_code_flow( 

539 session.get("flow", {}), auth_resp) 

540 if "error" in result: 

541 return render_template("error.html", result) 

542 store_tokens() 

543 except ValueError: # Usually caused by CSRF 

544 pass # Simply ignore them 

545 return redirect(url_for("index")) 

546 """ 

547 assert isinstance(auth_code_flow, dict) and isinstance(auth_response, dict) 

548 # This is app developer's error which we do NOT want to map to ValueError 

549 if not auth_code_flow.get("state"): 

550 # initiate_auth_code_flow() already guarantees a state to be available. 

551 # This check will also allow a web app to blindly call this method with 

552 # obtain_token_by_auth_code_flow(session.get("flow", {}), auth_resp) 

553 # which further simplifies their usage. 

554 raise ValueError("state missing from auth_code_flow") 

555 if auth_code_flow.get("state") != auth_response.get("state"): 

556 raise ValueError("state mismatch: {} vs {}".format( 

557 auth_code_flow.get("state"), auth_response.get("state"))) 

558 if scope and set(scope) - set(auth_code_flow.get("scope", [])): 

559 raise ValueError( 

560 "scope must be None or a subset of %s" % auth_code_flow.get("scope")) 

561 if auth_response.get("code"): # i.e. the first leg was successful 

562 return self._obtain_token_by_authorization_code( 

563 auth_response["code"], 

564 redirect_uri=auth_code_flow.get("redirect_uri"), 

565 # Required, if "redirect_uri" parameter was included in the 

566 # authorization request, and their values MUST be identical. 

567 scope=scope or auth_code_flow.get("scope"), 

568 # It is both unnecessary and harmless, per RFC 6749. 

569 # We use the same scope already used in auth request uri, 

570 # thus token cache can know what scope the tokens are for. 

571 data=dict( # Extract and update the data 

572 kwargs.pop("data", {}), 

573 code_verifier=auth_code_flow["code_verifier"], 

574 ), 

575 **kwargs) 

576 if auth_response.get("error"): # It means the first leg encountered error 

577 # Here we do NOT return original auth_response as-is, to prevent a 

578 # potential {..., "access_token": "attacker's AT"} input being leaked 

579 error = {"error": auth_response["error"]} 

580 if auth_response.get("error_description"): 

581 error["error_description"] = auth_response["error_description"] 

582 if auth_response.get("error_uri"): 

583 error["error_uri"] = auth_response["error_uri"] 

584 return error 

585 raise ValueError('auth_response must contain either "code" or "error"') 

586 

587 def obtain_token_by_browser( 

588 # Name influenced by RFC 8252: "native apps should (use) ... user's browser" 

589 self, 

590 redirect_uri=None, 

591 auth_code_receiver=None, 

592 **kwargs): 

593 """A native app can use this method to obtain token via a local browser. 

594 

595 Internally, it implements PKCE to mitigate the auth code interception attack. 

596 

597 :param scope: A list of scopes that you would like to obtain token for. 

598 :type scope: collections.Iterable[str] 

599 

600 :param extra_scope_to_consent: 

601 Some IdP allows you to include more scopes for end user to consent. 

602 The access token returned by this method will NOT include those scopes, 

603 but the refresh token would record those extra consent, 

604 so that your future :func:`~obtain_token_by_refresh_token()` call 

605 would be able to obtain token for those additional scopes, silently. 

606 :type scope: collections.Iterable[str] 

607 

608 :param string redirect_uri: 

609 The redirect_uri to be sent via auth request to Identity Provider (IdP), 

610 to indicate where an auth response would come back to. 

611 Such as ``http://127.0.0.1:0`` (default) or ``http://localhost:1234``. 

612 

613 If port 0 is specified, this method will choose a system-allocated port, 

614 then the actual redirect_uri will contain that port. 

615 To use this behavior, your IdP would need to accept such dynamic port. 

616 

617 Per HTTP convention, if port number is absent, it would mean port 80, 

618 although you probably want to specify port 0 in this context. 

619 

620 :param dict auth_params: 

621 These parameters will be sent to authorization_endpoint. 

622 

623 :param int timeout: In seconds. None means wait indefinitely. 

624 

625 :param str browser_name: 

626 If you did 

627 ``webbrowser.register("xyz", None, BackgroundBrowser("/path/to/browser"))`` 

628 beforehand, you can pass in the name "xyz" to use that browser. 

629 The default value ``None`` means using default browser, 

630 which is customizable by env var $BROWSER. 

631 

632 :return: Same as :func:`~obtain_token_by_auth_code_flow()` 

633 """ 

634 if auth_code_receiver: # Then caller already knows the listen port 

635 return self._obtain_token_by_browser( # Use all input param as-is 

636 auth_code_receiver, redirect_uri=redirect_uri, **kwargs) 

637 # Otherwise we will listen on _redirect_uri.port 

638 _redirect_uri = urlparse(redirect_uri or "http://127.0.0.1:0") 

639 if not _redirect_uri.hostname: 

640 raise ValueError("redirect_uri should contain hostname") 

641 listen_port = ( # Conventionally, port-less uri would mean port 80 

642 80 if _redirect_uri.port is None else _redirect_uri.port) 

643 try: 

644 with _AuthCodeReceiver(port=listen_port) as receiver: 

645 uri = redirect_uri if _redirect_uri.port != 0 else urlunparse(( 

646 _redirect_uri.scheme, 

647 "{}:{}".format(_redirect_uri.hostname, receiver.get_port()), 

648 _redirect_uri.path, 

649 _redirect_uri.params, 

650 _redirect_uri.query, 

651 _redirect_uri.fragment, 

652 )) # It could be slightly different than raw redirect_uri 

653 self.logger.debug("Using {} as redirect_uri".format(uri)) 

654 return self._obtain_token_by_browser( 

655 receiver, redirect_uri=uri, **kwargs) 

656 except PermissionError: 

657 raise ValueError( 

658 "Can't listen on port %s. You may try port 0." % listen_port) 

659 

660 def _obtain_token_by_browser( 

661 self, 

662 auth_code_receiver, 

663 scope=None, 

664 extra_scope_to_consent=None, 

665 redirect_uri=None, 

666 timeout=None, 

667 welcome_template=None, 

668 success_template=None, 

669 error_template=None, 

670 auth_params=None, 

671 auth_uri_callback=None, 

672 browser_name=None, 

673 **kwargs): 

674 # Internally, it calls self.initiate_auth_code_flow() and 

675 # self.obtain_token_by_auth_code_flow(). 

676 # 

677 # Parameters are documented in public method obtain_token_by_browser(). 

678 flow = self.initiate_auth_code_flow( 

679 redirect_uri=redirect_uri, 

680 scope=_scope_set(scope) | _scope_set(extra_scope_to_consent), 

681 response_mode='form_post', # The auth_code_receiver has been changed to require it 

682 **(auth_params or {})) 

683 auth_response = auth_code_receiver.get_auth_response( 

684 auth_uri=flow["auth_uri"], 

685 state=flow["state"], # So receiver can check it early 

686 timeout=timeout, 

687 welcome_template=welcome_template, 

688 success_template=success_template, 

689 error_template=error_template, 

690 auth_uri_callback=auth_uri_callback, 

691 browser_name=browser_name, 

692 ) 

693 if auth_response is None: 

694 raise BrowserInteractionTimeoutError("User did not complete the flow in time") 

695 return self.obtain_token_by_auth_code_flow( 

696 flow, auth_response, scope=scope, **kwargs) 

697 

698 @staticmethod 

699 def parse_auth_response(params, state=None): 

700 """Parse the authorization response being redirected back. 

701 

702 :param params: A string or dict of the query string 

703 :param state: REQUIRED if the state parameter was present in the client 

704 authorization request. This function will compare it with response. 

705 """ 

706 warnings.warn( 

707 "Use obtain_token_by_auth_code_flow() instead", DeprecationWarning) 

708 if not isinstance(params, dict): 

709 params = parse_qs(params) 

710 if params.get('state') != state: 

711 raise ValueError('state mismatch') 

712 return params 

713 

714 def obtain_token_by_authorization_code( 

715 self, code, redirect_uri=None, scope=None, **kwargs): 

716 """Get a token via authorization code. a.k.a. Authorization Code Grant. 

717 

718 This is typically used by a server-side app (Confidential Client), 

719 but it can also be used by a device-side native app (Public Client). 

720 See more detail at https://tools.ietf.org/html/rfc6749#section-4.1.3 

721 

722 You are encouraged to use its higher level method 

723 :func:`~obtain_token_by_auth_code_flow` instead. 

724 

725 :param code: The authorization code received from authorization server. 

726 :param redirect_uri: 

727 Required, if the "redirect_uri" parameter was included in the 

728 authorization request, and their values MUST be identical. 

729 :param scope: 

730 It is both unnecessary and harmless to use scope here, per RFC 6749. 

731 We suggest to use the same scope already used in auth request uri, 

732 so that this library can link the obtained tokens with their scope. 

733 """ 

734 warnings.warn( 

735 "Use obtain_token_by_auth_code_flow() instead", DeprecationWarning) 

736 return self._obtain_token_by_authorization_code( 

737 code, redirect_uri=redirect_uri, scope=scope, **kwargs) 

738 

739 def _obtain_token_by_authorization_code( 

740 self, code, redirect_uri=None, scope=None, **kwargs): 

741 data = kwargs.pop("data", {}) 

742 data.update(code=code, redirect_uri=redirect_uri) 

743 if scope: 

744 data["scope"] = scope 

745 if not self.client_secret: 

746 # client_id is required, if the client is not authenticating itself. 

747 # See https://tools.ietf.org/html/rfc6749#section-4.1.3 

748 data["client_id"] = self.client_id 

749 return self._obtain_token("authorization_code", data=data, **kwargs) 

750 

751 def obtain_token_by_username_password( 

752 self, username, password, scope=None, **kwargs): 

753 """The Resource Owner Password Credentials Grant, used by legacy app.""" 

754 data = kwargs.pop("data", {}) 

755 data.update(username=username, password=password, scope=scope) 

756 return self._obtain_token("password", data=data, **kwargs) 

757 

758 def obtain_token_for_client(self, scope=None, **kwargs): 

759 """Obtain token for this client (rather than for an end user), 

760 a.k.a. the Client Credentials Grant, used by Backend Applications. 

761 

762 We don't name it obtain_token_by_client_credentials(...) because those 

763 credentials are typically already provided in class constructor, not here. 

764 You can still explicitly provide an optional client_secret parameter, 

765 or you can provide such extra parameters as `default_body` during the 

766 class initialization. 

767 """ 

768 data = kwargs.pop("data", {}) 

769 data.update(scope=scope) 

770 return self._obtain_token("client_credentials", data=data, **kwargs) 

771 

772 def __init__(self, 

773 server_configuration, client_id, 

774 on_obtaining_tokens=lambda event: None, # event is defined in _obtain_token(...) 

775 on_removing_rt=lambda token_item: None, 

776 on_updating_rt=lambda token_item, new_rt: None, 

777 **kwargs): 

778 super(Client, self).__init__(server_configuration, client_id, **kwargs) 

779 self.on_obtaining_tokens = on_obtaining_tokens 

780 self.on_removing_rt = on_removing_rt 

781 self.on_updating_rt = on_updating_rt 

782 

783 def _obtain_token( 

784 self, grant_type, params=None, data=None, 

785 also_save_rt=False, 

786 on_obtaining_tokens=None, 

787 *args, **kwargs): 

788 _data = data.copy() # to prevent side effect 

789 resp = super(Client, self)._obtain_token( 

790 grant_type, params, _data, *args, **kwargs) 

791 if "error" not in resp: 

792 _resp = resp.copy() 

793 RT = "refresh_token" 

794 if grant_type == RT and RT in _resp and not also_save_rt: 

795 # Then we skip it from on_obtaining_tokens(); 

796 # Leave it to self.obtain_token_by_refresh_token() 

797 _resp.pop(RT, None) 

798 if "scope" in _resp: 

799 scope = _resp["scope"].split() # It is conceptually a set, 

800 # but we represent it as a list which can be persisted to JSON 

801 else: 

802 # Note: The scope will generally be absent in authorization grant, 

803 # but our obtain_token_by_authorization_code(...) encourages 

804 # app developer to still explicitly provide a scope here. 

805 scope = _data.get("scope") 

806 (on_obtaining_tokens or self.on_obtaining_tokens)({ 

807 "client_id": self.client_id, 

808 "scope": scope, 

809 "token_endpoint": self.configuration["token_endpoint"], 

810 "grant_type": grant_type, # can be used to know an IdToken-less 

811 # response is for an app or for a user 

812 "response": _resp, "params": params, "data": _data, 

813 }) 

814 return resp 

815 

816 def obtain_token_by_refresh_token(self, token_item, scope=None, 

817 rt_getter=lambda token_item: token_item["refresh_token"], 

818 on_removing_rt=None, 

819 on_updating_rt=None, 

820 **kwargs): 

821 # type: (Union[str, dict], Union[str, list, set, tuple], Callable) -> dict 

822 """This is an overload which will trigger token storage callbacks. 

823 

824 :param token_item: 

825 A refresh token (RT) item, in flexible format. It can be a string, 

826 or a whatever data structure containing RT string and its metadata, 

827 in such case the `rt_getter` callable must be able to 

828 extract the RT string out from the token item data structure. 

829 

830 Either way, this token_item will be passed into other callbacks as-is. 

831 

832 :param scope: If omitted, is treated as equal to the scope originally 

833 granted by the resource owner, 

834 according to https://tools.ietf.org/html/rfc6749#section-6 

835 :param rt_getter: A callable to translate the token_item to a raw RT string 

836 :param on_removing_rt: If absent, fall back to the one defined in initialization 

837 

838 :param on_updating_rt: 

839 Default to None, it will fall back to the one defined in initialization. 

840 This is the most common case. 

841 

842 As a special case, you can pass in a False, 

843 then this function will NOT trigger on_updating_rt() for RT UPDATE, 

844 instead it will allow the RT to be added by on_obtaining_tokens(). 

845 This behavior is useful when you are migrating RTs from elsewhere 

846 into a token storage managed by this library. 

847 """ 

848 resp = super(Client, self).obtain_token_by_refresh_token( 

849 rt_getter(token_item) 

850 if not isinstance(token_item, string_types) else token_item, 

851 scope=scope, 

852 also_save_rt=on_updating_rt is False, 

853 **kwargs) 

854 if resp.get('error') == 'invalid_grant': 

855 (on_removing_rt or self.on_removing_rt)(token_item) # Discard old RT 

856 RT = "refresh_token" 

857 if on_updating_rt is not False and RT in resp: 

858 (on_updating_rt or self.on_updating_rt)(token_item, resp[RT]) 

859 return resp 

860 

861 def obtain_token_by_assertion( 

862 self, assertion, grant_type, scope=None, **kwargs): 

863 # type: (bytes, Union[str, None], Union[str, list, set, tuple]) -> dict 

864 """This method implements Assertion Framework for OAuth2 (RFC 7521). 

865 See details at https://tools.ietf.org/html/rfc7521#section-4.1 

866 

867 :param assertion: 

868 The assertion bytes can be a raw SAML2 assertion, or a JWT assertion. 

869 :param grant_type: 

870 It is typically either the value of :attr:`GRANT_TYPE_SAML2`, 

871 or :attr:`GRANT_TYPE_JWT`, the only two profiles defined in RFC 7521. 

872 :param scope: Optional. It must be a subset of previously granted scopes. 

873 """ 

874 encoder = self.grant_assertion_encoders.get(grant_type, lambda a: a) 

875 data = kwargs.pop("data", {}) 

876 data.update(scope=scope, assertion=encoder(assertion)) 

877 return self._obtain_token(grant_type, data=data, **kwargs) 

878