Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
oauth2.py861 linesDownload Raw Back to oauth2cli
1"""This OAuth2 client implementation aims to be spec-compliant, and generic."""2# OAuth2 spec https://tools.ietf.org/html/rfc67493 4import json5try:6    from urllib.parse import urlencode, parse_qs, quote_plus, urlparse, urlunparse7except ImportError:8    from urlparse import parse_qs, urlparse, urlunparse9    from urllib import urlencode, quote_plus10import logging11import warnings12import time13import base6414import sys15import functools16import random17import string18import hashlib19 20from .authcode import AuthCodeReceiver as _AuthCodeReceiver21 22try:23    PermissionError  # Available in Python 324except:25    from socket import error as PermissionError  # Workaround for Python 226 27 28string_types = (str,) if sys.version_info[0] >= 3 else (basestring, )29 30 31class BaseClient(object):32    # This low-level interface works. Yet you'll find its sub-class33    # more friendly to remind you what parameters are needed in each scenario.34    # More on Client Types at https://tools.ietf.org/html/rfc6749#section-2.135 36    @staticmethod37    def encode_saml_assertion(assertion):38        return base64.urlsafe_b64encode(assertion).rstrip(b'=')  # Per RFC 752239 40    CLIENT_ASSERTION_TYPE_JWT = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"41    CLIENT_ASSERTION_TYPE_SAML2 = "urn:ietf:params:oauth:client-assertion-type:saml2-bearer"42    client_assertion_encoders = {CLIENT_ASSERTION_TYPE_SAML2: encode_saml_assertion}43 44    @property45    def session(self):46        warnings.warn("Will be gone in next major release", DeprecationWarning)47        return self._http_client48 49    @session.setter50    def session(self, value):51        warnings.warn("Will be gone in next major release", DeprecationWarning)52        self._http_client = value53 54 55    def __init__(56            self,57            server_configuration,  # type: dict58            client_id,  # type: str59            http_client=None,  # We insert it here to match the upcoming async API60            client_secret=None,  # type: Optional[str]61            client_assertion=None,  # type: Union[bytes, callable, None]62            client_assertion_type=None,  # type: Optional[str]63            default_headers=None,  # type: Optional[dict]64            default_body=None,  # type: Optional[dict]65            verify=None,  # type: Union[str, True, False, None]66            proxies=None,  # type: Optional[dict]67            timeout=None,  # type: Union[tuple, float, None]68            ):69        """Initialize a client object to talk all the OAuth2 grants to the server.70 71        Args:72            server_configuration (dict):73                It contains the configuration (i.e. metadata) of the auth server.74                The actual content typically contains keys like75                "authorization_endpoint", "token_endpoint", etc..76                Based on RFC 8414 (https://tools.ietf.org/html/rfc8414),77                you can probably fetch it online from either78                https://example.com/.../.well-known/oauth-authorization-server79                or80                https://example.com/.../.well-known/openid-configuration81            client_id (str): The client's id, issued by the authorization server82 83            http_client (http.HttpClient):84                Your implementation of abstract class :class:`http.HttpClient`.85                Defaults to a requests session instance.86 87                There is no session-wide `timeout` parameter defined here.88                Timeout behavior is determined by the actual http client you use.89                If you happen to use Requests, it disallows session-wide timeout90                (https://github.com/psf/requests/issues/3341). The workaround is:91 92                    s = requests.Session()93                    s.request = functools.partial(s.request, timeout=3)94 95                and then feed that patched session instance to this class.96 97            client_secret (str):  Triggers HTTP AUTH for Confidential Client98            client_assertion (bytes, callable):99                The client assertion to authenticate this client, per RFC 7521.100                It can be a raw SAML2 assertion (we will base64 encode it for you),101                or a raw JWT assertion in bytes (which we will relay to http layer).102                It can also be a callable (recommended),103                so that we will do lazy creation of an assertion.104            client_assertion_type (str):105                The type of your :attr:`client_assertion` parameter.106                It is typically the value of :attr:`CLIENT_ASSERTION_TYPE_SAML2` or107                :attr:`CLIENT_ASSERTION_TYPE_JWT`, the only two defined in RFC 7521.108            default_headers (dict):109                A dict to be sent in each request header.110                It is not required by OAuth2 specs, but you may use it for telemetry.111            default_body (dict):112                A dict to be sent in each token request body. For example,113                you could choose to set this as {"client_secret": "your secret"}114                if your authorization server wants it to be in the request body115                (rather than in the request header).116 117            verify (boolean):118                It will be passed to the119                `verify parameter in the underlying requests library120                <http://docs.python-requests.org/en/v2.9.1/user/advanced/#ssl-cert-verification>`_.121                When leaving it with default value (None), we will use True instead.122 123                This does not apply if you have chosen to pass your own Http client.124 125            proxies (dict):126                It will be passed to the127                `proxies parameter in the underlying requests library128                <http://docs.python-requests.org/en/v2.9.1/user/advanced/#proxies>`_.129 130                This does not apply if you have chosen to pass your own Http client.131 132            timeout (object):133                It will be passed to the134                `timeout parameter in the underlying requests library135                <http://docs.python-requests.org/en/v2.9.1/user/advanced/#timeouts>`_.136 137                This does not apply if you have chosen to pass your own Http client.138 139        """140        if not server_configuration:141            raise ValueError("Missing input parameter server_configuration")142        # Generally we should have client_id, but we tolerate its absence143        self.configuration = server_configuration144        self.client_id = client_id145        self.client_secret = client_secret146        self.client_assertion = client_assertion147        self.default_headers = default_headers or {}148        self.default_body = default_body or {}149        if client_assertion_type is not None:150            self.default_body["client_assertion_type"] = client_assertion_type151        self.logger = logging.getLogger(__name__)152        if http_client:153            if verify is not None or proxies is not None or timeout is not None:154                raise ValueError(155                    "verify, proxies, or timeout is not allowed "156                    "when http_client is in use")157            self._http_client = http_client158        else:159            import requests  # Lazy loading160 161            self._http_client = requests.Session()162            self._http_client.verify = True if verify is None else verify163            self._http_client.proxies = proxies164            self._http_client.request = functools.partial(165                # A workaround for requests not supporting session-wide timeout166                self._http_client.request, timeout=timeout)167 168    def _build_auth_request_params(self, response_type, **kwargs):169        # response_type is a string defined in170        #   https://tools.ietf.org/html/rfc6749#section-3.1.1171        # or it can be a space-delimited string as defined in172        #   https://tools.ietf.org/html/rfc6749#section-8.4173        response_type = self._stringify(response_type)174 175        params = {'client_id': self.client_id, 'response_type': response_type}176        params.update(kwargs)  # Note: None values will override params177        params = {k: v for k, v in params.items() if v is not None}  # clean up178        if params.get('scope'):179            params['scope'] = self._stringify(params['scope'])180        return params  # A dict suitable to be used in http request181 182    def _obtain_token(  # The verb "obtain" is influenced by OAUTH2 RFC 6749183            self, grant_type,184            params=None,  # a dict to be sent as query string to the endpoint185            data=None,  # All relevant data, which will go into the http body186            headers=None,  # a dict to be sent as request headers187            post=None,  # A callable to replace requests.post(), for testing.188                        # Such as: lambda url, **kwargs:189                        #   Mock(status_code=200, text='{}')190            **kwargs  # Relay all extra parameters to underlying requests191            ):  # Returns the json object came from the OAUTH2 response192        _data = {'client_id': self.client_id, 'grant_type': grant_type}193 194        if self.default_body.get("client_assertion_type") and self.client_assertion:195            # See https://tools.ietf.org/html/rfc7521#section-4.2196            encoder = self.client_assertion_encoders.get(197                    self.default_body["client_assertion_type"], lambda a: a)198            _data["client_assertion"] = encoder(199                self.client_assertion()  # Do lazy on-the-fly computation200                if callable(self.client_assertion) else self.client_assertion201                )   # The type is bytes, which is preferable. See also:202                    # https://github.com/psf/requests/issues/4503#issuecomment-455001070203 204        _data.update(self.default_body)  # It may contain authen parameters205        _data.update(data or {})  # So the content in data param prevails206        _data = {k: v for k, v in _data.items() if v}  # Clean up None values207 208        if _data.get('scope'):209            _data['scope'] = self._stringify(_data['scope'])210 211        _headers = {'Accept': 'application/json'}212        _headers.update(self.default_headers)213        _headers.update(headers or {})214 215        # Quoted from https://tools.ietf.org/html/rfc6749#section-2.3.1216        # Clients in possession of a client password MAY use the HTTP Basic217        # authentication.218        # Alternatively, (but NOT RECOMMENDED,)219        # the authorization server MAY support including the220        # client credentials in the request-body using the following221        # parameters: client_id, client_secret.222        if self.client_secret and self.client_id:223            _headers["Authorization"] = "Basic " + base64.b64encode("{}:{}".format(224                # Per https://tools.ietf.org/html/rfc6749#section-2.3.1225                # client_id and client_secret needs to be encoded by226                # "application/x-www-form-urlencoded"227                # https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1228                # BEFORE they are fed into HTTP Basic Authentication229                quote_plus(self.client_id), quote_plus(self.client_secret)230                ).encode("ascii")).decode("ascii")231 232        if "token_endpoint" not in self.configuration:233            raise ValueError("token_endpoint not found in configuration")234        resp = (post or self._http_client.post)(235            self.configuration["token_endpoint"],236            headers=_headers, params=params, data=_data,237            **kwargs)238        if resp.status_code >= 500:239            resp.raise_for_status()  # TODO: Will probably retry here240        try:241            # The spec (https://tools.ietf.org/html/rfc6749#section-5.2) says242            # even an error response will be a valid json structure,243            # so we simply return it here, without needing to invent an exception.244            return json.loads(resp.text)245        except ValueError:246            self.logger.exception(247                    "Token response is not in json format: %s", resp.text)248            raise249 250    def obtain_token_by_refresh_token(self, refresh_token, scope=None, **kwargs):251        # type: (str, Union[str, list, set, tuple]) -> dict252        """Obtain an access token via a refresh token.253 254        :param refresh_token: The refresh token issued to the client255        :param scope: If omitted, is treated as equal to the scope originally256            granted by the resource owner,257            according to https://tools.ietf.org/html/rfc6749#section-6258        """259        assert isinstance(refresh_token, string_types)260        data = kwargs.pop('data', {})261        data.update(refresh_token=refresh_token, scope=scope)262        return self._obtain_token("refresh_token", data=data, **kwargs)263 264    def _stringify(self, sequence):265        if isinstance(sequence, (list, set, tuple)):266            return ' '.join(sorted(sequence))  # normalizing it, ascendingly267        return sequence  # as-is268 269 270def _scope_set(scope):271    assert scope is None or isinstance(scope, (list, set, tuple))272    return set(scope) if scope else set([])273 274 275def _generate_pkce_code_verifier(length=43):276    assert 43 <= length <= 128277    verifier = "".join(  # https://tools.ietf.org/html/rfc7636#section-4.1278        random.sample(string.ascii_letters + string.digits + "-._~", length))279    code_challenge = (280        # https://tools.ietf.org/html/rfc7636#section-4.2281        base64.urlsafe_b64encode(hashlib.sha256(verifier.encode("ascii")).digest())282        .rstrip(b"="))  # Required by https://tools.ietf.org/html/rfc7636#section-3283    return {284        "code_verifier": verifier,285        "transformation": "S256",  # In Python, sha256 is always available286        "code_challenge": code_challenge,287        }288 289 290class Client(BaseClient):  # We choose to implement all 4 grants in 1 class291    """This is the main API for oauth2 client.292 293    Its methods define and document parameters mentioned in OAUTH2 RFC 6749.294    """295    DEVICE_FLOW = {  # consts for device flow, that can be customized by sub-class296        "GRANT_TYPE": "urn:ietf:params:oauth:grant-type:device_code",297        "DEVICE_CODE": "device_code",298        }299    DEVICE_FLOW_RETRIABLE_ERRORS = ("authorization_pending", "slow_down")300    GRANT_TYPE_SAML2 = "urn:ietf:params:oauth:grant-type:saml2-bearer"  # RFC7522301    GRANT_TYPE_JWT = "urn:ietf:params:oauth:grant-type:jwt-bearer"  # RFC7523302    grant_assertion_encoders = {GRANT_TYPE_SAML2: BaseClient.encode_saml_assertion}303 304 305    def initiate_device_flow(self, scope=None, **kwargs):306        # type: (list, **dict) -> dict307        # The naming of this method is following the wording of this specs308        # https://tools.ietf.org/html/draft-ietf-oauth-device-flow-12#section-3.1309        """Initiate a device flow.310 311        Returns the data defined in Device Flow specs.312        https://tools.ietf.org/html/draft-ietf-oauth-device-flow-12#section-3.2313 314        You should then orchestrate the User Interaction as defined in here315        https://tools.ietf.org/html/draft-ietf-oauth-device-flow-12#section-3.3316 317        And possibly here318        https://tools.ietf.org/html/draft-ietf-oauth-device-flow-12#section-3.3.1319        """320        DAE = "device_authorization_endpoint"321        if not self.configuration.get(DAE):322            raise ValueError("You need to provide device authorization endpoint")323        resp = self._http_client.post(self.configuration[DAE],324            data={"client_id": self.client_id, "scope": self._stringify(scope or [])},325            headers=dict(self.default_headers, **kwargs.pop("headers", {})),326            **kwargs)327        flow = json.loads(resp.text)328        flow["interval"] = int(flow.get("interval", 5))  # Some IdP returns string329        flow["expires_in"] = int(flow.get("expires_in", 1800))330        flow["expires_at"] = time.time() + flow["expires_in"]  # We invent this331        return flow332 333    def _obtain_token_by_device_flow(self, flow, **kwargs):334        # type: (dict, **dict) -> dict335        # This method updates flow during each run. And it is non-blocking.336        now = time.time()337        skew = 1338        if flow.get("latest_attempt_at", 0) + flow.get("interval", 5) - skew > now:339            warnings.warn('Attempted too soon. Please do time.sleep(flow["interval"])')340        data = kwargs.pop("data", {})341        data.update({342            "client_id": self.client_id,343            self.DEVICE_FLOW["DEVICE_CODE"]: flow["device_code"],344            })345        result = self._obtain_token(346            self.DEVICE_FLOW["GRANT_TYPE"], data=data, **kwargs)347        if result.get("error") == "slow_down":348            # Respecting https://tools.ietf.org/html/draft-ietf-oauth-device-flow-12#section-3.5349            flow["interval"] = flow.get("interval", 5) + 5350        flow["latest_attempt_at"] = now351        return result352 353    def obtain_token_by_device_flow(self,354            flow,355            exit_condition=lambda flow: flow.get("expires_at", 0) < time.time(),356            **kwargs):357        # type: (dict, Callable) -> dict358        """Obtain token by a device flow object, with customizable polling effect.359 360        Args:361            flow (dict):362                An object previously generated by initiate_device_flow(...).363                Its content WILL BE CHANGED by this method during each run.364                We share this object with you, so that you could implement365                your own loop, should you choose to do so.366 367            exit_condition (Callable):368                This method implements a loop to provide polling effect.369                The loop's exit condition is calculated by this callback.370 371                The default callback makes the loop run until the flow expires.372                Therefore, one of the ways to exit the polling early,373                is to change the flow["expires_at"] to a small number such as 0.374 375                In case you are doing async programming, you may want to376                completely turn off the loop. You can do so by using a callback as:377 378                    exit_condition = lambda flow: True379 380                to make the loop run only once, i.e. no polling, hence non-block.381        """382        while True:383            result = self._obtain_token_by_device_flow(flow, **kwargs)384            if result.get("error") not in self.DEVICE_FLOW_RETRIABLE_ERRORS:385                return result386            for i in range(flow.get("interval", 5)):  # Wait interval seconds387                if exit_condition(flow):388                    return result389                time.sleep(1)  # Shorten each round, to make exit more responsive390 391    def _build_auth_request_uri(392            self,393            response_type, redirect_uri=None, scope=None, state=None, **kwargs):394        if "authorization_endpoint" not in self.configuration:395            raise ValueError("authorization_endpoint not found in configuration")396        authorization_endpoint = self.configuration["authorization_endpoint"]397        params = self._build_auth_request_params(398            response_type, redirect_uri=redirect_uri, scope=scope, state=state,399            **kwargs)400        sep = '&' if '?' in authorization_endpoint else '?'401        return "%s%s%s" % (authorization_endpoint, sep, urlencode(params))402 403    def build_auth_request_uri(404            self,405            response_type, redirect_uri=None, scope=None, state=None, **kwargs):406        # This method could be named build_authorization_request_uri() instead,407        # but then there would be a build_authentication_request_uri() in the OIDC408        # subclass doing almost the same thing. So we use a loose term "auth" here.409        """Generate an authorization uri to be visited by resource owner.410 411        Parameters are the same as another method :func:`initiate_auth_code_flow()`,412        whose functionality is a superset of this method.413 414        :return: The auth uri as a string.415        """416        warnings.warn("Use initiate_auth_code_flow() instead. ", DeprecationWarning)417        return self._build_auth_request_uri(418            response_type, redirect_uri=redirect_uri, scope=scope, state=state,419            **kwargs)420 421    def initiate_auth_code_flow(422        # The name is influenced by OIDC423        # https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth424            self,425            scope=None, redirect_uri=None, state=None,426            **kwargs):427        """Initiate an auth code flow.428 429        Later when the response reaches your redirect_uri,430        you can use :func:`~obtain_token_by_auth_code_flow()`431        to complete the authentication/authorization.432 433        This method also provides PKCE protection automatically.434 435        :param list scope:436            It is a list of case-sensitive strings.437            Some ID provider can accept empty string to represent default scope.438        :param str redirect_uri:439            Optional. If not specified, server will use the pre-registered one.440        :param str state:441            An opaque value used by the client to442            maintain state between the request and callback.443            If absent, this library will automatically generate one internally.444        :param kwargs: Other parameters, typically defined in OpenID Connect.445 446        :return:447            The auth code flow. It is a dict in this form::448 449                {450                    "auth_uri": "https://...",  // Guide user to visit this451                    "state": "...",  // You may choose to verify it by yourself,452                                     // or just let obtain_token_by_auth_code_flow()453                                     // do that for you.454                    "...": "...",  // Everything else are reserved and internal455                }456 457            The caller is expected to::458 459            1. somehow store this content, typically inside the current session,460            2. guide the end user (i.e. resource owner) to visit that auth_uri,461            3. and then relay this dict and subsequent auth response to462               :func:`~obtain_token_by_auth_code_flow()`.463        """464        response_type = kwargs.pop("response_type", "code")  # Auth Code flow465            # Must be "code" when you are using Authorization Code Grant.466            # The "token" for Implicit Grant is not applicable thus not allowed.467            # It could theoretically be other468            # (possibly space-delimited) strings as registered extension value.469            # See https://tools.ietf.org/html/rfc6749#section-3.1.1470        if "token" in response_type:471            # Implicit grant would cause auth response coming back in #fragment,472            # but fragment won't reach a web service.473            raise ValueError('response_type="token ..." is not allowed')474        pkce = _generate_pkce_code_verifier()475        flow = {  # These data are required by obtain_token_by_auth_code_flow()476            "state": state or "".join(random.sample(string.ascii_letters, 16)),477            "redirect_uri": redirect_uri,478            "scope": scope,479            }480        auth_uri = self._build_auth_request_uri(481            response_type,482            code_challenge=pkce["code_challenge"],483            code_challenge_method=pkce["transformation"],484            **dict(flow, **kwargs))485        flow["auth_uri"] = auth_uri486        flow["code_verifier"] = pkce["code_verifier"]487        return flow488 489    def obtain_token_by_auth_code_flow(490            self,491            auth_code_flow,492            auth_response,493            scope=None,494            **kwargs):495        """With the auth_response being redirected back,496        validate it against auth_code_flow, and then obtain tokens.497 498        Internally, it implements PKCE to mitigate the auth code interception attack.499 500        :param dict auth_code_flow:501            The same dict returned by :func:`~initiate_auth_code_flow()`.502        :param dict auth_response:503            A dict based on query string received from auth server.504 505        :param scope:506            You don't usually need to use scope parameter here.507            Some Identity Provider allows you to provide508            a subset of what you specified during :func:`~initiate_auth_code_flow`.509        :type scope: collections.Iterable[str]510 511        :return:512            * A dict containing "access_token" and/or "id_token", among others,513              depends on what scope was used.514              (See https://tools.ietf.org/html/rfc6749#section-5.1)515            * A dict containing "error", optionally "error_description", "error_uri".516              (It is either `this <https://tools.ietf.org/html/rfc6749#section-4.1.2.1>`_517              or `that <https://tools.ietf.org/html/rfc6749#section-5.2>`_518            * Most client-side data error would result in ValueError exception.519              So the usage pattern could be without any protocol details::520 521                def authorize():  # A controller in a web app522                    try:523                        result = client.obtain_token_by_auth_code_flow(524                            session.get("flow", {}), auth_resp)525                        if "error" in result:526                            return render_template("error.html", result)527                        store_tokens()528                    except ValueError:  # Usually caused by CSRF529                        pass  # Simply ignore them530                    return redirect(url_for("index"))531        """532        assert isinstance(auth_code_flow, dict) and isinstance(auth_response, dict)533            # This is app developer's error which we do NOT want to map to ValueError534        if not auth_code_flow.get("state"):535            # initiate_auth_code_flow() already guarantees a state to be available.536            # This check will also allow a web app to blindly call this method with537            # obtain_token_by_auth_code_flow(session.get("flow", {}), auth_resp)538            # which further simplifies their usage.539            raise ValueError("state missing from auth_code_flow")540        if auth_code_flow.get("state") != auth_response.get("state"):541            raise ValueError("state mismatch: {} vs {}".format(542                auth_code_flow.get("state"), auth_response.get("state")))543        if scope and set(scope) - set(auth_code_flow.get("scope", [])):544            raise ValueError(545                "scope must be None or a subset of %s" % auth_code_flow.get("scope"))546        if auth_response.get("code"):  # i.e. the first leg was successful547            return self._obtain_token_by_authorization_code(548                auth_response["code"],549                redirect_uri=auth_code_flow.get("redirect_uri"),550                    # Required, if "redirect_uri" parameter was included in the551                    # authorization request, and their values MUST be identical.552                scope=scope or auth_code_flow.get("scope"),553                    # It is both unnecessary and harmless, per RFC 6749.554                    # We use the same scope already used in auth request uri,555                    # thus token cache can know what scope the tokens are for.556                data=dict(  # Extract and update the data557                    kwargs.pop("data", {}),558                    code_verifier=auth_code_flow["code_verifier"],559                    ),560                **kwargs)561        if auth_response.get("error"):  # It means the first leg encountered error562            # Here we do NOT return original auth_response as-is, to prevent a563            # potential {..., "access_token": "attacker's AT"} input being leaked564            error = {"error": auth_response["error"]}565            if auth_response.get("error_description"):566                error["error_description"] = auth_response["error_description"]567            if auth_response.get("error_uri"):568                error["error_uri"] = auth_response["error_uri"]569            return error570        raise ValueError('auth_response must contain either "code" or "error"')571 572    def obtain_token_by_browser(573        # Name influenced by RFC 8252: "native apps should (use) ... user's browser"574            self,575            redirect_uri=None,576            auth_code_receiver=None,577            **kwargs):578        """A native app can use this method to obtain token via a local browser.579 580        Internally, it implements PKCE to mitigate the auth code interception attack.581 582        :param scope: A list of scopes that you would like to obtain token for.583        :type scope: collections.Iterable[str]584 585        :param extra_scope_to_consent:586            Some IdP allows you to include more scopes for end user to consent.587            The access token returned by this method will NOT include those scopes,588            but the refresh token would record those extra consent,589            so that your future :func:`~obtain_token_by_refresh_token()` call590            would be able to obtain token for those additional scopes, silently.591        :type scope: collections.Iterable[str]592 593        :param string redirect_uri:594            The redirect_uri to be sent via auth request to Identity Provider (IdP),595            to indicate where an auth response would come back to.596            Such as ``http://127.0.0.1:0`` (default) or ``http://localhost:1234``.597 598            If port 0 is specified, this method will choose a system-allocated port,599            then the actual redirect_uri will contain that port.600            To use this behavior, your IdP would need to accept such dynamic port.601 602            Per HTTP convention, if port number is absent, it would mean port 80,603            although you probably want to specify port 0 in this context.604 605        :param dict auth_params:606            These parameters will be sent to authorization_endpoint.607 608        :param int timeout: In seconds. None means wait indefinitely.609 610        :param str browser_name:611            If you did612            ``webbrowser.register("xyz", None, BackgroundBrowser("/path/to/browser"))``613            beforehand, you can pass in the name "xyz" to use that browser.614            The default value ``None`` means using default browser,615            which is customizable by env var $BROWSER.616 617        :return: Same as :func:`~obtain_token_by_auth_code_flow()`618        """619        if auth_code_receiver:  # Then caller already knows the listen port620            return self._obtain_token_by_browser(  # Use all input param as-is621                auth_code_receiver, redirect_uri=redirect_uri, **kwargs)622        # Otherwise we will listen on _redirect_uri.port623        _redirect_uri = urlparse(redirect_uri or "http://127.0.0.1:0")624        if not _redirect_uri.hostname:625            raise ValueError("redirect_uri should contain hostname")626        listen_port = (  # Conventionally, port-less uri would mean port 80627            80 if _redirect_uri.port is None else _redirect_uri.port)628        try:629            with _AuthCodeReceiver(port=listen_port) as receiver:630                uri = redirect_uri if _redirect_uri.port != 0 else urlunparse((631                    _redirect_uri.scheme,632                    "{}:{}".format(_redirect_uri.hostname, receiver.get_port()),633                    _redirect_uri.path,634                    _redirect_uri.params,635                    _redirect_uri.query,636                    _redirect_uri.fragment,637                    ))  # It could be slightly different than raw redirect_uri638                self.logger.debug("Using {} as redirect_uri".format(uri))639                return self._obtain_token_by_browser(640                    receiver, redirect_uri=uri, **kwargs)641        except PermissionError:642            raise ValueError(643                "Can't listen on port %s. You may try port 0." % listen_port)644 645    def _obtain_token_by_browser(646            self,647            auth_code_receiver,648            scope=None,649            extra_scope_to_consent=None,650            redirect_uri=None,651            timeout=None,652            welcome_template=None,653            success_template=None,654            error_template=None,655            auth_params=None,656            auth_uri_callback=None,657            browser_name=None,658            **kwargs):659        # Internally, it calls self.initiate_auth_code_flow() and660        # self.obtain_token_by_auth_code_flow().661        #662        # Parameters are documented in public method obtain_token_by_browser().663        flow = self.initiate_auth_code_flow(664            redirect_uri=redirect_uri,665            scope=_scope_set(scope) | _scope_set(extra_scope_to_consent),666            **(auth_params or {}))667        auth_response = auth_code_receiver.get_auth_response(668            auth_uri=flow["auth_uri"],669            state=flow["state"],  # So receiver can check it early670            timeout=timeout,671            welcome_template=welcome_template,672            success_template=success_template,673            error_template=error_template,674            auth_uri_callback=auth_uri_callback,675            browser_name=browser_name,676            )677        return self.obtain_token_by_auth_code_flow(678            flow, auth_response, scope=scope, **kwargs)679 680    @staticmethod681    def parse_auth_response(params, state=None):682        """Parse the authorization response being redirected back.683 684        :param params: A string or dict of the query string685        :param state: REQUIRED if the state parameter was present in the client686            authorization request. This function will compare it with response.687        """688        warnings.warn(689            "Use obtain_token_by_auth_code_flow() instead", DeprecationWarning)690        if not isinstance(params, dict):691            params = parse_qs(params)692        if params.get('state') != state:693            raise ValueError('state mismatch')694        return params695 696    def obtain_token_by_authorization_code(697            self, code, redirect_uri=None, scope=None, **kwargs):698        """Get a token via authorization code. a.k.a. Authorization Code Grant.699 700        This is typically used by a server-side app (Confidential Client),701        but it can also be used by a device-side native app (Public Client).702        See more detail at https://tools.ietf.org/html/rfc6749#section-4.1.3703 704        You are encouraged to use its higher level method705        :func:`~obtain_token_by_auth_code_flow` instead.706 707        :param code: The authorization code received from authorization server.708        :param redirect_uri:709            Required, if the "redirect_uri" parameter was included in the710            authorization request, and their values MUST be identical.711        :param scope:712            It is both unnecessary and harmless to use scope here, per RFC 6749.713            We suggest to use the same scope already used in auth request uri,714            so that this library can link the obtained tokens with their scope.715        """716        warnings.warn(717            "Use obtain_token_by_auth_code_flow() instead", DeprecationWarning)718        return self._obtain_token_by_authorization_code(719            code, redirect_uri=redirect_uri, scope=scope, **kwargs)720 721    def _obtain_token_by_authorization_code(722            self, code, redirect_uri=None, scope=None, **kwargs):723        data = kwargs.pop("data", {})724        data.update(code=code, redirect_uri=redirect_uri)725        if scope:726            data["scope"] = scope727        if not self.client_secret:728            # client_id is required, if the client is not authenticating itself.729            # See https://tools.ietf.org/html/rfc6749#section-4.1.3730            data["client_id"] = self.client_id731        return self._obtain_token("authorization_code", data=data, **kwargs)732 733    def obtain_token_by_username_password(734            self, username, password, scope=None, **kwargs):735        """The Resource Owner Password Credentials Grant, used by legacy app."""736        data = kwargs.pop("data", {})737        data.update(username=username, password=password, scope=scope)738        return self._obtain_token("password", data=data, **kwargs)739 740    def obtain_token_for_client(self, scope=None, **kwargs):741        """Obtain token for this client (rather than for an end user),742        a.k.a. the Client Credentials Grant, used by Backend Applications.743 744        We don't name it obtain_token_by_client_credentials(...) because those745        credentials are typically already provided in class constructor, not here.746        You can still explicitly provide an optional client_secret parameter,747        or you can provide such extra parameters as `default_body` during the748        class initialization.749        """750        data = kwargs.pop("data", {})751        data.update(scope=scope)752        return self._obtain_token("client_credentials", data=data, **kwargs)753 754    def __init__(self,755            server_configuration, client_id,756            on_obtaining_tokens=lambda event: None,  # event is defined in _obtain_token(...)757            on_removing_rt=lambda token_item: None,758            on_updating_rt=lambda token_item, new_rt: None,759            **kwargs):760        super(Client, self).__init__(server_configuration, client_id, **kwargs)761        self.on_obtaining_tokens = on_obtaining_tokens762        self.on_removing_rt = on_removing_rt763        self.on_updating_rt = on_updating_rt764 765    def _obtain_token(766            self, grant_type, params=None, data=None,767            also_save_rt=False,768            on_obtaining_tokens=None,769            *args, **kwargs):770        _data = data.copy()  # to prevent side effect771        resp = super(Client, self)._obtain_token(772            grant_type, params, _data, *args, **kwargs)773        if "error" not in resp:774            _resp = resp.copy()775            RT = "refresh_token"776            if grant_type == RT and RT in _resp and not also_save_rt:777                # Then we skip it from on_obtaining_tokens();778                # Leave it to self.obtain_token_by_refresh_token()779                _resp.pop(RT, None)780            if "scope" in _resp:781                scope = _resp["scope"].split()  # It is conceptually a set,782                    # but we represent it as a list which can be persisted to JSON783            else:784                # Note: The scope will generally be absent in authorization grant,785                #       but our obtain_token_by_authorization_code(...) encourages786                #       app developer to still explicitly provide a scope here.787                scope = _data.get("scope")788            (on_obtaining_tokens or self.on_obtaining_tokens)({789                "client_id": self.client_id,790                "scope": scope,791                "token_endpoint": self.configuration["token_endpoint"],792                "grant_type": grant_type,  # can be used to know an IdToken-less793                                           # response is for an app or for a user794                "response": _resp, "params": params, "data": _data,795                })796        return resp797 798    def obtain_token_by_refresh_token(self, token_item, scope=None,799            rt_getter=lambda token_item: token_item["refresh_token"],800            on_removing_rt=None,801            on_updating_rt=None,802            **kwargs):803        # type: (Union[str, dict], Union[str, list, set, tuple], Callable) -> dict804        """This is an overload which will trigger token storage callbacks.805 806        :param token_item:807            A refresh token (RT) item, in flexible format. It can be a string,808            or a whatever data structure containing RT string and its metadata,809            in such case the `rt_getter` callable must be able to810            extract the RT string out from the token item data structure.811 812            Either way, this token_item will be passed into other callbacks as-is.813 814        :param scope: If omitted, is treated as equal to the scope originally815            granted by the resource owner,816            according to https://tools.ietf.org/html/rfc6749#section-6817        :param rt_getter: A callable to translate the token_item to a raw RT string818        :param on_removing_rt: If absent, fall back to the one defined in initialization819 820        :param on_updating_rt:821            Default to None, it will fall back to the one defined in initialization.822            This is the most common case.823 824            As a special case, you can pass in a False,825            then this function will NOT trigger on_updating_rt() for RT UPDATE,826            instead it will allow the RT to be added by on_obtaining_tokens().827            This behavior is useful when you are migrating RTs from elsewhere828            into a token storage managed by this library.829        """830        resp = super(Client, self).obtain_token_by_refresh_token(831            rt_getter(token_item)832                if not isinstance(token_item, string_types) else token_item,833            scope=scope,834            also_save_rt=on_updating_rt is False,835            **kwargs)836        if resp.get('error') == 'invalid_grant':837            (on_removing_rt or self.on_removing_rt)(token_item)  # Discard old RT838        RT = "refresh_token"839        if on_updating_rt is not False and RT in resp:840            (on_updating_rt or self.on_updating_rt)(token_item, resp[RT])841        return resp842 843    def obtain_token_by_assertion(844            self, assertion, grant_type, scope=None, **kwargs):845        # type: (bytes, Union[str, None], Union[str, list, set, tuple]) -> dict846        """This method implements Assertion Framework for OAuth2 (RFC 7521).847        See details at https://tools.ietf.org/html/rfc7521#section-4.1848 849        :param assertion:850            The assertion bytes can be a raw SAML2 assertion, or a JWT assertion.851        :param grant_type:852            It is typically either the value of :attr:`GRANT_TYPE_SAML2`,853            or :attr:`GRANT_TYPE_JWT`, the only two profiles defined in RFC 7521.854        :param scope: Optional. It must be a subset of previously granted scopes.855        """856        encoder = self.grant_assertion_encoders.get(grant_type, lambda a: a)857        data = kwargs.pop("data", {})858        data.update(scope=scope, assertion=encoder(assertion))859        return self._obtain_token(grant_type, data=data, **kwargs)860 861 
codekingpro/portable-devtools · Team Ai