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, redirect_uri=None, scope=None, state=None, **kwargs):
400 if "authorization_endpoint" not in self.configuration:
401 raise ValueError("authorization_endpoint not found in configuration")
402 authorization_endpoint = self.configuration["authorization_endpoint"]
403 params = self._build_auth_request_params(
404 response_type, redirect_uri=redirect_uri, scope=scope, state=state,
405 **kwargs)
406 sep = '&' if '?' in authorization_endpoint else '?'
407 return "%s%s%s" % (authorization_endpoint, sep, urlencode(params))
408
409 def build_auth_request_uri(
410 self,
411 response_type, redirect_uri=None, scope=None, state=None, **kwargs):
412 # This method could be named build_authorization_request_uri() instead,
413 # but then there would be a build_authentication_request_uri() in the OIDC
414 # subclass doing almost the same thing. So we use a loose term "auth" here.
415 """Generate an authorization uri to be visited by resource owner.
416
417 Parameters are the same as another method :func:`initiate_auth_code_flow()`,
418 whose functionality is a superset of this method.
419
420 :return: The auth uri as a string.
421 """
422 warnings.warn("Use initiate_auth_code_flow() instead. ", DeprecationWarning)
423 return self._build_auth_request_uri(
424 response_type, redirect_uri=redirect_uri, scope=scope, state=state,
425 **kwargs)
426
427 def initiate_auth_code_flow(
428 # The name is influenced by OIDC
429 # https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth
430 self,
431 scope=None, redirect_uri=None, state=None,
432 **kwargs):
433 """Initiate an auth code flow.
434
435 Later when the response reaches your redirect_uri,
436 you can use :func:`~obtain_token_by_auth_code_flow()`
437 to complete the authentication/authorization.
438
439 This method also provides PKCE protection automatically.
440
441 :param list scope:
442 It is a list of case-sensitive strings.
443 Some ID provider can accept empty string to represent default scope.
444 :param str redirect_uri:
445 Optional. If not specified, server will use the pre-registered one.
446 :param str state:
447 An opaque value used by the client to
448 maintain state between the request and callback.
449 If absent, this library will automatically generate one internally.
450 :param kwargs: Other parameters, typically defined in OpenID Connect.
451
452 :return:
453 The auth code flow. It is a dict in this form::
454
455 {
456 "auth_uri": "https://...", // Guide user to visit this
457 "state": "...", // You may choose to verify it by yourself,
458 // or just let obtain_token_by_auth_code_flow()
459 // do that for you.
460 "...": "...", // Everything else are reserved and internal
461 }
462
463 The caller is expected to::
464
465 1. somehow store this content, typically inside the current session,
466 2. guide the end user (i.e. resource owner) to visit that auth_uri,
467 3. and then relay this dict and subsequent auth response to
468 :func:`~obtain_token_by_auth_code_flow()`.
469 """
470 response_type = kwargs.pop("response_type", "code") # Auth Code flow
471 # Must be "code" when you are using Authorization Code Grant.
472 # The "token" for Implicit Grant is not applicable thus not allowed.
473 # It could theoretically be other
474 # (possibly space-delimited) strings as registered extension value.
475 # See https://tools.ietf.org/html/rfc6749#section-3.1.1
476 if "token" in response_type:
477 # Implicit grant would cause auth response coming back in #fragment,
478 # but fragment won't reach a web service.
479 raise ValueError('response_type="token ..." is not allowed')
480 pkce = _generate_pkce_code_verifier()
481 flow = { # These data are required by obtain_token_by_auth_code_flow()
482 "state": state or "".join(random.sample(string.ascii_letters, 16)),
483 "redirect_uri": redirect_uri,
484 "scope": scope,
485 }
486 auth_uri = self._build_auth_request_uri(
487 response_type,
488 code_challenge=pkce["code_challenge"],
489 code_challenge_method=pkce["transformation"],
490 **dict(flow, **kwargs))
491 flow["auth_uri"] = auth_uri
492 flow["code_verifier"] = pkce["code_verifier"]
493 return flow
494
495 def obtain_token_by_auth_code_flow(
496 self,
497 auth_code_flow,
498 auth_response,
499 scope=None,
500 **kwargs):
501 """With the auth_response being redirected back,
502 validate it against auth_code_flow, and then obtain tokens.
503
504 Internally, it implements PKCE to mitigate the auth code interception attack.
505
506 :param dict auth_code_flow:
507 The same dict returned by :func:`~initiate_auth_code_flow()`.
508 :param dict auth_response:
509 A dict based on query string received from auth server.
510
511 :param scope:
512 You don't usually need to use scope parameter here.
513 Some Identity Provider allows you to provide
514 a subset of what you specified during :func:`~initiate_auth_code_flow`.
515 :type scope: collections.Iterable[str]
516
517 :return:
518 * A dict containing "access_token" and/or "id_token", among others,
519 depends on what scope was used.
520 (See https://tools.ietf.org/html/rfc6749#section-5.1)
521 * A dict containing "error", optionally "error_description", "error_uri".
522 (It is either `this <https://tools.ietf.org/html/rfc6749#section-4.1.2.1>`_
523 or `that <https://tools.ietf.org/html/rfc6749#section-5.2>`_
524 * Most client-side data error would result in ValueError exception.
525 So the usage pattern could be without any protocol details::
526
527 def authorize(): # A controller in a web app
528 try:
529 result = client.obtain_token_by_auth_code_flow(
530 session.get("flow", {}), auth_resp)
531 if "error" in result:
532 return render_template("error.html", result)
533 store_tokens()
534 except ValueError: # Usually caused by CSRF
535 pass # Simply ignore them
536 return redirect(url_for("index"))
537 """
538 assert isinstance(auth_code_flow, dict) and isinstance(auth_response, dict)
539 # This is app developer's error which we do NOT want to map to ValueError
540 if not auth_code_flow.get("state"):
541 # initiate_auth_code_flow() already guarantees a state to be available.
542 # This check will also allow a web app to blindly call this method with
543 # obtain_token_by_auth_code_flow(session.get("flow", {}), auth_resp)
544 # which further simplifies their usage.
545 raise ValueError("state missing from auth_code_flow")
546 if auth_code_flow.get("state") != auth_response.get("state"):
547 raise ValueError("state mismatch: {} vs {}".format(
548 auth_code_flow.get("state"), auth_response.get("state")))
549 if scope and set(scope) - set(auth_code_flow.get("scope", [])):
550 raise ValueError(
551 "scope must be None or a subset of %s" % auth_code_flow.get("scope"))
552 if auth_response.get("code"): # i.e. the first leg was successful
553 return self._obtain_token_by_authorization_code(
554 auth_response["code"],
555 redirect_uri=auth_code_flow.get("redirect_uri"),
556 # Required, if "redirect_uri" parameter was included in the
557 # authorization request, and their values MUST be identical.
558 scope=scope or auth_code_flow.get("scope"),
559 # It is both unnecessary and harmless, per RFC 6749.
560 # We use the same scope already used in auth request uri,
561 # thus token cache can know what scope the tokens are for.
562 data=dict( # Extract and update the data
563 kwargs.pop("data", {}),
564 code_verifier=auth_code_flow["code_verifier"],
565 ),
566 **kwargs)
567 if auth_response.get("error"): # It means the first leg encountered error
568 # Here we do NOT return original auth_response as-is, to prevent a
569 # potential {..., "access_token": "attacker's AT"} input being leaked
570 error = {"error": auth_response["error"]}
571 if auth_response.get("error_description"):
572 error["error_description"] = auth_response["error_description"]
573 if auth_response.get("error_uri"):
574 error["error_uri"] = auth_response["error_uri"]
575 return error
576 raise ValueError('auth_response must contain either "code" or "error"')
577
578 def obtain_token_by_browser(
579 # Name influenced by RFC 8252: "native apps should (use) ... user's browser"
580 self,
581 redirect_uri=None,
582 auth_code_receiver=None,
583 **kwargs):
584 """A native app can use this method to obtain token via a local browser.
585
586 Internally, it implements PKCE to mitigate the auth code interception attack.
587
588 :param scope: A list of scopes that you would like to obtain token for.
589 :type scope: collections.Iterable[str]
590
591 :param extra_scope_to_consent:
592 Some IdP allows you to include more scopes for end user to consent.
593 The access token returned by this method will NOT include those scopes,
594 but the refresh token would record those extra consent,
595 so that your future :func:`~obtain_token_by_refresh_token()` call
596 would be able to obtain token for those additional scopes, silently.
597 :type scope: collections.Iterable[str]
598
599 :param string redirect_uri:
600 The redirect_uri to be sent via auth request to Identity Provider (IdP),
601 to indicate where an auth response would come back to.
602 Such as ``http://127.0.0.1:0`` (default) or ``http://localhost:1234``.
603
604 If port 0 is specified, this method will choose a system-allocated port,
605 then the actual redirect_uri will contain that port.
606 To use this behavior, your IdP would need to accept such dynamic port.
607
608 Per HTTP convention, if port number is absent, it would mean port 80,
609 although you probably want to specify port 0 in this context.
610
611 :param dict auth_params:
612 These parameters will be sent to authorization_endpoint.
613
614 :param int timeout: In seconds. None means wait indefinitely.
615
616 :param str browser_name:
617 If you did
618 ``webbrowser.register("xyz", None, BackgroundBrowser("/path/to/browser"))``
619 beforehand, you can pass in the name "xyz" to use that browser.
620 The default value ``None`` means using default browser,
621 which is customizable by env var $BROWSER.
622
623 :return: Same as :func:`~obtain_token_by_auth_code_flow()`
624 """
625 if auth_code_receiver: # Then caller already knows the listen port
626 return self._obtain_token_by_browser( # Use all input param as-is
627 auth_code_receiver, redirect_uri=redirect_uri, **kwargs)
628 # Otherwise we will listen on _redirect_uri.port
629 _redirect_uri = urlparse(redirect_uri or "http://127.0.0.1:0")
630 if not _redirect_uri.hostname:
631 raise ValueError("redirect_uri should contain hostname")
632 listen_port = ( # Conventionally, port-less uri would mean port 80
633 80 if _redirect_uri.port is None else _redirect_uri.port)
634 try:
635 with _AuthCodeReceiver(port=listen_port) as receiver:
636 uri = redirect_uri if _redirect_uri.port != 0 else urlunparse((
637 _redirect_uri.scheme,
638 "{}:{}".format(_redirect_uri.hostname, receiver.get_port()),
639 _redirect_uri.path,
640 _redirect_uri.params,
641 _redirect_uri.query,
642 _redirect_uri.fragment,
643 )) # It could be slightly different than raw redirect_uri
644 self.logger.debug("Using {} as redirect_uri".format(uri))
645 return self._obtain_token_by_browser(
646 receiver, redirect_uri=uri, **kwargs)
647 except PermissionError:
648 raise ValueError(
649 "Can't listen on port %s. You may try port 0." % listen_port)
650
651 def _obtain_token_by_browser(
652 self,
653 auth_code_receiver,
654 scope=None,
655 extra_scope_to_consent=None,
656 redirect_uri=None,
657 timeout=None,
658 welcome_template=None,
659 success_template=None,
660 error_template=None,
661 auth_params=None,
662 auth_uri_callback=None,
663 browser_name=None,
664 **kwargs):
665 # Internally, it calls self.initiate_auth_code_flow() and
666 # self.obtain_token_by_auth_code_flow().
667 #
668 # Parameters are documented in public method obtain_token_by_browser().
669 flow = self.initiate_auth_code_flow(
670 redirect_uri=redirect_uri,
671 scope=_scope_set(scope) | _scope_set(extra_scope_to_consent),
672 **(auth_params or {}))
673 auth_response = auth_code_receiver.get_auth_response(
674 auth_uri=flow["auth_uri"],
675 state=flow["state"], # So receiver can check it early
676 timeout=timeout,
677 welcome_template=welcome_template,
678 success_template=success_template,
679 error_template=error_template,
680 auth_uri_callback=auth_uri_callback,
681 browser_name=browser_name,
682 )
683 if auth_response is None:
684 raise BrowserInteractionTimeoutError("User did not complete the flow in time")
685 return self.obtain_token_by_auth_code_flow(
686 flow, auth_response, scope=scope, **kwargs)
687
688 @staticmethod
689 def parse_auth_response(params, state=None):
690 """Parse the authorization response being redirected back.
691
692 :param params: A string or dict of the query string
693 :param state: REQUIRED if the state parameter was present in the client
694 authorization request. This function will compare it with response.
695 """
696 warnings.warn(
697 "Use obtain_token_by_auth_code_flow() instead", DeprecationWarning)
698 if not isinstance(params, dict):
699 params = parse_qs(params)
700 if params.get('state') != state:
701 raise ValueError('state mismatch')
702 return params
703
704 def obtain_token_by_authorization_code(
705 self, code, redirect_uri=None, scope=None, **kwargs):
706 """Get a token via authorization code. a.k.a. Authorization Code Grant.
707
708 This is typically used by a server-side app (Confidential Client),
709 but it can also be used by a device-side native app (Public Client).
710 See more detail at https://tools.ietf.org/html/rfc6749#section-4.1.3
711
712 You are encouraged to use its higher level method
713 :func:`~obtain_token_by_auth_code_flow` instead.
714
715 :param code: The authorization code received from authorization server.
716 :param redirect_uri:
717 Required, if the "redirect_uri" parameter was included in the
718 authorization request, and their values MUST be identical.
719 :param scope:
720 It is both unnecessary and harmless to use scope here, per RFC 6749.
721 We suggest to use the same scope already used in auth request uri,
722 so that this library can link the obtained tokens with their scope.
723 """
724 warnings.warn(
725 "Use obtain_token_by_auth_code_flow() instead", DeprecationWarning)
726 return self._obtain_token_by_authorization_code(
727 code, redirect_uri=redirect_uri, scope=scope, **kwargs)
728
729 def _obtain_token_by_authorization_code(
730 self, code, redirect_uri=None, scope=None, **kwargs):
731 data = kwargs.pop("data", {})
732 data.update(code=code, redirect_uri=redirect_uri)
733 if scope:
734 data["scope"] = scope
735 if not self.client_secret:
736 # client_id is required, if the client is not authenticating itself.
737 # See https://tools.ietf.org/html/rfc6749#section-4.1.3
738 data["client_id"] = self.client_id
739 return self._obtain_token("authorization_code", data=data, **kwargs)
740
741 def obtain_token_by_username_password(
742 self, username, password, scope=None, **kwargs):
743 """The Resource Owner Password Credentials Grant, used by legacy app."""
744 data = kwargs.pop("data", {})
745 data.update(username=username, password=password, scope=scope)
746 return self._obtain_token("password", data=data, **kwargs)
747
748 def obtain_token_for_client(self, scope=None, **kwargs):
749 """Obtain token for this client (rather than for an end user),
750 a.k.a. the Client Credentials Grant, used by Backend Applications.
751
752 We don't name it obtain_token_by_client_credentials(...) because those
753 credentials are typically already provided in class constructor, not here.
754 You can still explicitly provide an optional client_secret parameter,
755 or you can provide such extra parameters as `default_body` during the
756 class initialization.
757 """
758 data = kwargs.pop("data", {})
759 data.update(scope=scope)
760 return self._obtain_token("client_credentials", data=data, **kwargs)
761
762 def __init__(self,
763 server_configuration, client_id,
764 on_obtaining_tokens=lambda event: None, # event is defined in _obtain_token(...)
765 on_removing_rt=lambda token_item: None,
766 on_updating_rt=lambda token_item, new_rt: None,
767 **kwargs):
768 super(Client, self).__init__(server_configuration, client_id, **kwargs)
769 self.on_obtaining_tokens = on_obtaining_tokens
770 self.on_removing_rt = on_removing_rt
771 self.on_updating_rt = on_updating_rt
772
773 def _obtain_token(
774 self, grant_type, params=None, data=None,
775 also_save_rt=False,
776 on_obtaining_tokens=None,
777 *args, **kwargs):
778 _data = data.copy() # to prevent side effect
779 resp = super(Client, self)._obtain_token(
780 grant_type, params, _data, *args, **kwargs)
781 if "error" not in resp:
782 _resp = resp.copy()
783 RT = "refresh_token"
784 if grant_type == RT and RT in _resp and not also_save_rt:
785 # Then we skip it from on_obtaining_tokens();
786 # Leave it to self.obtain_token_by_refresh_token()
787 _resp.pop(RT, None)
788 if "scope" in _resp:
789 scope = _resp["scope"].split() # It is conceptually a set,
790 # but we represent it as a list which can be persisted to JSON
791 else:
792 # Note: The scope will generally be absent in authorization grant,
793 # but our obtain_token_by_authorization_code(...) encourages
794 # app developer to still explicitly provide a scope here.
795 scope = _data.get("scope")
796 (on_obtaining_tokens or self.on_obtaining_tokens)({
797 "client_id": self.client_id,
798 "scope": scope,
799 "token_endpoint": self.configuration["token_endpoint"],
800 "grant_type": grant_type, # can be used to know an IdToken-less
801 # response is for an app or for a user
802 "response": _resp, "params": params, "data": _data,
803 })
804 return resp
805
806 def obtain_token_by_refresh_token(self, token_item, scope=None,
807 rt_getter=lambda token_item: token_item["refresh_token"],
808 on_removing_rt=None,
809 on_updating_rt=None,
810 **kwargs):
811 # type: (Union[str, dict], Union[str, list, set, tuple], Callable) -> dict
812 """This is an overload which will trigger token storage callbacks.
813
814 :param token_item:
815 A refresh token (RT) item, in flexible format. It can be a string,
816 or a whatever data structure containing RT string and its metadata,
817 in such case the `rt_getter` callable must be able to
818 extract the RT string out from the token item data structure.
819
820 Either way, this token_item will be passed into other callbacks as-is.
821
822 :param scope: If omitted, is treated as equal to the scope originally
823 granted by the resource owner,
824 according to https://tools.ietf.org/html/rfc6749#section-6
825 :param rt_getter: A callable to translate the token_item to a raw RT string
826 :param on_removing_rt: If absent, fall back to the one defined in initialization
827
828 :param on_updating_rt:
829 Default to None, it will fall back to the one defined in initialization.
830 This is the most common case.
831
832 As a special case, you can pass in a False,
833 then this function will NOT trigger on_updating_rt() for RT UPDATE,
834 instead it will allow the RT to be added by on_obtaining_tokens().
835 This behavior is useful when you are migrating RTs from elsewhere
836 into a token storage managed by this library.
837 """
838 resp = super(Client, self).obtain_token_by_refresh_token(
839 rt_getter(token_item)
840 if not isinstance(token_item, string_types) else token_item,
841 scope=scope,
842 also_save_rt=on_updating_rt is False,
843 **kwargs)
844 if resp.get('error') == 'invalid_grant':
845 (on_removing_rt or self.on_removing_rt)(token_item) # Discard old RT
846 RT = "refresh_token"
847 if on_updating_rt is not False and RT in resp:
848 (on_updating_rt or self.on_updating_rt)(token_item, resp[RT])
849 return resp
850
851 def obtain_token_by_assertion(
852 self, assertion, grant_type, scope=None, **kwargs):
853 # type: (bytes, Union[str, None], Union[str, list, set, tuple]) -> dict
854 """This method implements Assertion Framework for OAuth2 (RFC 7521).
855 See details at https://tools.ietf.org/html/rfc7521#section-4.1
856
857 :param assertion:
858 The assertion bytes can be a raw SAML2 assertion, or a JWT assertion.
859 :param grant_type:
860 It is typically either the value of :attr:`GRANT_TYPE_SAML2`,
861 or :attr:`GRANT_TYPE_JWT`, the only two profiles defined in RFC 7521.
862 :param scope: Optional. It must be a subset of previously granted scopes.
863 """
864 encoder = self.grant_assertion_encoders.get(grant_type, lambda a: a)
865 data = kwargs.pop("data", {})
866 data.update(scope=scope, assertion=encoder(assertion))
867 return self._obtain_token(grant_type, data=data, **kwargs)
868