Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
auth.py1236 linesDownload Raw Back to tornado
1#
2# Copyright 2009 Facebook
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8#     http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16"""This module contains implementations of various third-party
17authentication schemes.
18
19All the classes in this file are class mixins designed to be used with
20the `tornado.web.RequestHandler` class.  They are used in two ways:
21
22* On a login handler, use methods such as ``authenticate_redirect()``,
23  ``authorize_redirect()``, and ``get_authenticated_user()`` to
24  establish the user's identity and store authentication tokens to your
25  database and/or cookies.
26* In non-login handlers, use methods such as ``facebook_request()``
27  or ``twitter_request()`` to use the authentication tokens to make
28  requests to the respective services.
29
30They all take slightly different arguments due to the fact all these
31services implement authentication and authorization slightly differently.
32See the individual service classes below for complete documentation.
33
34Example usage for Google OAuth:
35
36.. testsetup::
37
38    import urllib
39
40.. testcode::
41
42    class GoogleOAuth2LoginHandler(tornado.web.RequestHandler,
43                                    tornado.auth.GoogleOAuth2Mixin):
44        async def get(self):
45            # Google requires an exact match for redirect_uri, so it's
46            # best to get it from your app configuration instead of from
47            # self.request.full_uri().
48            redirect_uri = urllib.parse.urljoin(self.application.settings['redirect_base_uri'],
49                self.reverse_url('google_oauth'))
50            async def get(self):
51                if self.get_argument('code', False):
52                    access = await self.get_authenticated_user(
53                        redirect_uri=redirect_uri,
54                        code=self.get_argument('code'))
55                    user = await self.oauth2_request(
56                        "https://www.googleapis.com/oauth2/v1/userinfo",
57                        access_token=access["access_token"])
58                    # Save the user and access token. For example:
59                    user_cookie = dict(id=user["id"], access_token=access["access_token"])
60                    self.set_signed_cookie("user", json.dumps(user_cookie))
61                    self.redirect("/")
62                else:
63                    self.authorize_redirect(
64                        redirect_uri=redirect_uri,
65                        client_id=self.get_google_oauth_settings()['key'],
66                        scope=['profile', 'email'],
67                        response_type='code',
68                        extra_params={'approval_prompt': 'auto'})
69
70"""
71
72import base64
73import binascii
74import hashlib
75import hmac
76import time
77import urllib.parse
78import uuid
79import warnings
80
81from tornado import httpclient
82from tornado import escape
83from tornado.httputil import url_concat
84from tornado.util import unicode_type
85from tornado.web import RequestHandler
86
87from typing import List, Any, Dict, cast, Iterable, Union, Optional
88
89
90class AuthError(Exception):
91    pass
92
93
94class OpenIdMixin:
95    """Abstract implementation of OpenID and Attribute Exchange.
96
97    Class attributes:
98
99    * ``_OPENID_ENDPOINT``: the identity provider's URI.
100    """
101
102    def authenticate_redirect(
103        self,
104        callback_uri: Optional[str] = None,
105        ax_attrs: List[str] = ["name", "email", "language", "username"],
106    ) -> None:
107        """Redirects to the authentication URL for this service.
108
109        After authentication, the service will redirect back to the given
110        callback URI with additional parameters including ``openid.mode``.
111
112        We request the given attributes for the authenticated user by
113        default (name, email, language, and username). If you don't need
114        all those attributes for your app, you can request fewer with
115        the ax_attrs keyword argument.
116
117        .. versionchanged:: 6.0
118
119            The ``callback`` argument was removed and this method no
120            longer returns an awaitable object. It is now an ordinary
121            synchronous function.
122        """
123        handler = cast(RequestHandler, self)
124        callback_uri = callback_uri or handler.request.uri
125        assert callback_uri is not None
126        args = self._openid_args(callback_uri, ax_attrs=ax_attrs)
127        endpoint = self._OPENID_ENDPOINT  # type: ignore
128        handler.redirect(endpoint + "?" + urllib.parse.urlencode(args))
129
130    async def get_authenticated_user(
131        self, http_client: Optional[httpclient.AsyncHTTPClient] = None
132    ) -> Dict[str, Any]:
133        """Fetches the authenticated user data upon redirect.
134
135        This method should be called by the handler that receives the
136        redirect from the `authenticate_redirect()` method (which is
137        often the same as the one that calls it; in that case you would
138        call `get_authenticated_user` if the ``openid.mode`` parameter
139        is present and `authenticate_redirect` if it is not).
140
141        The result of this method will generally be used to set a cookie.
142
143        .. versionchanged:: 6.0
144
145            The ``callback`` argument was removed. Use the returned
146            awaitable object instead.
147        """
148        handler = cast(RequestHandler, self)
149        # Verify the OpenID response via direct request to the OP
150        args = {
151            k: v[-1] for k, v in handler.request.arguments.items()
152        }  # type: Dict[str, Union[str, bytes]]
153        args["openid.mode"] = "check_authentication"
154        url = self._OPENID_ENDPOINT  # type: ignore
155        if http_client is None:
156            http_client = self.get_auth_http_client()
157        resp = await http_client.fetch(
158            url, method="POST", body=urllib.parse.urlencode(args)
159        )
160        return self._on_authentication_verified(resp)
161
162    def _openid_args(
163        self,
164        callback_uri: str,
165        ax_attrs: Iterable[str] = [],
166        oauth_scope: Optional[str] = None,
167    ) -> Dict[str, str]:
168        handler = cast(RequestHandler, self)
169        url = urllib.parse.urljoin(handler.request.full_url(), callback_uri)
170        args = {
171            "openid.ns": "http://specs.openid.net/auth/2.0",
172            "openid.claimed_id": "http://specs.openid.net/auth/2.0/identifier_select",
173            "openid.identity": "http://specs.openid.net/auth/2.0/identifier_select",
174            "openid.return_to": url,
175            "openid.realm": urllib.parse.urljoin(url, "/"),
176            "openid.mode": "checkid_setup",
177        }
178        if ax_attrs:
179            args.update(
180                {
181                    "openid.ns.ax": "http://openid.net/srv/ax/1.0",
182                    "openid.ax.mode": "fetch_request",
183                }
184            )
185            ax_attrs = set(ax_attrs)
186            required = []  # type: List[str]
187            if "name" in ax_attrs:
188                ax_attrs -= {"name", "firstname", "fullname", "lastname"}
189                required += ["firstname", "fullname", "lastname"]
190                args.update(
191                    {
192                        "openid.ax.type.firstname": "http://axschema.org/namePerson/first",
193                        "openid.ax.type.fullname": "http://axschema.org/namePerson",
194                        "openid.ax.type.lastname": "http://axschema.org/namePerson/last",
195                    }
196                )
197            known_attrs = {
198                "email": "http://axschema.org/contact/email",
199                "language": "http://axschema.org/pref/language",
200                "username": "http://axschema.org/namePerson/friendly",
201            }
202            for name in ax_attrs:
203                args["openid.ax.type." + name] = known_attrs[name]
204                required.append(name)
205            args["openid.ax.required"] = ",".join(required)
206        if oauth_scope:
207            args.update(
208                {
209                    "openid.ns.oauth": "http://specs.openid.net/extensions/oauth/1.0",
210                    "openid.oauth.consumer": handler.request.host.split(":")[0],
211                    "openid.oauth.scope": oauth_scope,
212                }
213            )
214        return args
215
216    def _on_authentication_verified(
217        self, response: httpclient.HTTPResponse
218    ) -> Dict[str, Any]:
219        handler = cast(RequestHandler, self)
220        if b"is_valid:true" not in response.body:
221            raise AuthError("Invalid OpenID response: %r" % response.body)
222
223        # Make sure we got back at least an email from attribute exchange
224        ax_ns = None
225        for key in handler.request.arguments:
226            if (
227                key.startswith("openid.ns.")
228                and handler.get_argument(key) == "http://openid.net/srv/ax/1.0"
229            ):
230                ax_ns = key[10:]
231                break
232
233        def get_ax_arg(uri: str) -> str:
234            if not ax_ns:
235                return ""
236            prefix = "openid." + ax_ns + ".type."
237            ax_name = None
238            for name in handler.request.arguments.keys():
239                if handler.get_argument(name) == uri and name.startswith(prefix):
240                    part = name[len(prefix) :]
241                    ax_name = "openid." + ax_ns + ".value." + part
242                    break
243            if not ax_name:
244                return ""
245            return handler.get_argument(ax_name, "")
246
247        email = get_ax_arg("http://axschema.org/contact/email")
248        name = get_ax_arg("http://axschema.org/namePerson")
249        first_name = get_ax_arg("http://axschema.org/namePerson/first")
250        last_name = get_ax_arg("http://axschema.org/namePerson/last")
251        username = get_ax_arg("http://axschema.org/namePerson/friendly")
252        locale = get_ax_arg("http://axschema.org/pref/language").lower()
253        user = dict()
254        name_parts = []
255        if first_name:
256            user["first_name"] = first_name
257            name_parts.append(first_name)
258        if last_name:
259            user["last_name"] = last_name
260            name_parts.append(last_name)
261        if name:
262            user["name"] = name
263        elif name_parts:
264            user["name"] = " ".join(name_parts)
265        elif email:
266            user["name"] = email.split("@")[0]
267        if email:
268            user["email"] = email
269        if locale:
270            user["locale"] = locale
271        if username:
272            user["username"] = username
273        claimed_id = handler.get_argument("openid.claimed_id", None)
274        if claimed_id:
275            user["claimed_id"] = claimed_id
276        return user
277
278    def get_auth_http_client(self) -> httpclient.AsyncHTTPClient:
279        """Returns the `.AsyncHTTPClient` instance to be used for auth requests.
280
281        May be overridden by subclasses to use an HTTP client other than
282        the default.
283        """
284        return httpclient.AsyncHTTPClient()
285
286
287class OAuthMixin:
288    """Abstract implementation of OAuth 1.0 and 1.0a.
289
290    See `TwitterMixin` below for an example implementation.
291
292    Class attributes:
293
294    * ``_OAUTH_AUTHORIZE_URL``: The service's OAuth authorization url.
295    * ``_OAUTH_ACCESS_TOKEN_URL``: The service's OAuth access token url.
296    * ``_OAUTH_VERSION``: May be either "1.0" or "1.0a".
297    * ``_OAUTH_NO_CALLBACKS``: Set this to True if the service requires
298      advance registration of callbacks.
299
300    Subclasses must also override the `_oauth_get_user_future` and
301    `_oauth_consumer_token` methods.
302    """
303
304    async def authorize_redirect(
305        self,
306        callback_uri: Optional[str] = None,
307        extra_params: Optional[Dict[str, Any]] = None,
308        http_client: Optional[httpclient.AsyncHTTPClient] = None,
309    ) -> None:
310        """Redirects the user to obtain OAuth authorization for this service.
311
312        The ``callback_uri`` may be omitted if you have previously
313        registered a callback URI with the third-party service. For
314        some services, you must use a previously-registered callback
315        URI and cannot specify a callback via this method.
316
317        This method sets a cookie called ``_oauth_request_token`` which is
318        subsequently used (and cleared) in `get_authenticated_user` for
319        security purposes.
320
321        This method is asynchronous and must be called with ``await``
322        or ``yield`` (This is different from other ``auth*_redirect``
323        methods defined in this module). It calls
324        `.RequestHandler.finish` for you so you should not write any
325        other response after it returns.
326
327        .. versionchanged:: 3.1
328           Now returns a `.Future` and takes an optional callback, for
329           compatibility with `.gen.coroutine`.
330
331        .. versionchanged:: 6.0
332
333           The ``callback`` argument was removed. Use the returned
334           awaitable object instead.
335
336        """
337        if callback_uri and getattr(self, "_OAUTH_NO_CALLBACKS", False):
338            raise Exception("This service does not support oauth_callback")
339        if http_client is None:
340            http_client = self.get_auth_http_client()
341        assert http_client is not None
342        if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
343            response = await http_client.fetch(
344                self._oauth_request_token_url(
345                    callback_uri=callback_uri, extra_params=extra_params
346                )
347            )
348        else:
349            response = await http_client.fetch(self._oauth_request_token_url())
350        url = self._OAUTH_AUTHORIZE_URL  # type: ignore
351        self._on_request_token(url, callback_uri, response)
352
353    async def get_authenticated_user(
354        self, http_client: Optional[httpclient.AsyncHTTPClient] = None
355    ) -> Dict[str, Any]:
356        """Gets the OAuth authorized user and access token.
357
358        This method should be called from the handler for your
359        OAuth callback URL to complete the registration process. We run the
360        callback with the authenticated user dictionary.  This dictionary
361        will contain an ``access_key`` which can be used to make authorized
362        requests to this service on behalf of the user.  The dictionary will
363        also contain other fields such as ``name``, depending on the service
364        used.
365
366        .. versionchanged:: 6.0
367
368           The ``callback`` argument was removed. Use the returned
369           awaitable object instead.
370        """
371        handler = cast(RequestHandler, self)
372        request_key = escape.utf8(handler.get_argument("oauth_token"))
373        oauth_verifier = handler.get_argument("oauth_verifier", None)
374        request_cookie = handler.get_cookie("_oauth_request_token")
375        if not request_cookie:
376            raise AuthError("Missing OAuth request token cookie")
377        handler.clear_cookie("_oauth_request_token")
378        cookie_key, cookie_secret = (
379            base64.b64decode(escape.utf8(i)) for i in request_cookie.split("|")
380        )
381        if cookie_key != request_key:
382            raise AuthError("Request token does not match cookie")
383        token = dict(
384            key=cookie_key, secret=cookie_secret
385        )  # type: Dict[str, Union[str, bytes]]
386        if oauth_verifier:
387            token["verifier"] = oauth_verifier
388        if http_client is None:
389            http_client = self.get_auth_http_client()
390        assert http_client is not None
391        response = await http_client.fetch(self._oauth_access_token_url(token))
392        access_token = _oauth_parse_response(response.body)
393        user = await self._oauth_get_user_future(access_token)
394        if not user:
395            raise AuthError("Error getting user")
396        user["access_token"] = access_token
397        return user
398
399    def _oauth_request_token_url(
400        self,
401        callback_uri: Optional[str] = None,
402        extra_params: Optional[Dict[str, Any]] = None,
403    ) -> str:
404        handler = cast(RequestHandler, self)
405        consumer_token = self._oauth_consumer_token()
406        url = self._OAUTH_REQUEST_TOKEN_URL  # type: ignore
407        args = dict(
408            oauth_consumer_key=escape.to_basestring(consumer_token["key"]),
409            oauth_signature_method="HMAC-SHA1",
410            oauth_timestamp=str(int(time.time())),
411            oauth_nonce=escape.to_basestring(binascii.b2a_hex(uuid.uuid4().bytes)),
412            oauth_version="1.0",
413        )
414        if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
415            if callback_uri == "oob":
416                args["oauth_callback"] = "oob"
417            elif callback_uri:
418                args["oauth_callback"] = urllib.parse.urljoin(
419                    handler.request.full_url(), callback_uri
420                )
421            if extra_params:
422                args.update(extra_params)
423            signature = _oauth10a_signature(consumer_token, "GET", url, args)
424        else:
425            signature = _oauth_signature(consumer_token, "GET", url, args)
426
427        args["oauth_signature"] = signature
428        return url + "?" + urllib.parse.urlencode(args)
429
430    def _on_request_token(
431        self,
432        authorize_url: str,
433        callback_uri: Optional[str],
434        response: httpclient.HTTPResponse,
435    ) -> None:
436        handler = cast(RequestHandler, self)
437        request_token = _oauth_parse_response(response.body)
438        data = (
439            base64.b64encode(escape.utf8(request_token["key"]))
440            + b"|"
441            + base64.b64encode(escape.utf8(request_token["secret"]))
442        )
443        handler.set_cookie("_oauth_request_token", data)
444        args = dict(oauth_token=request_token["key"])
445        if callback_uri == "oob":
446            handler.finish(authorize_url + "?" + urllib.parse.urlencode(args))
447            return
448        elif callback_uri:
449            args["oauth_callback"] = urllib.parse.urljoin(
450                handler.request.full_url(), callback_uri
451            )
452        handler.redirect(authorize_url + "?" + urllib.parse.urlencode(args))
453
454    def _oauth_access_token_url(self, request_token: Dict[str, Any]) -> str:
455        consumer_token = self._oauth_consumer_token()
456        url = self._OAUTH_ACCESS_TOKEN_URL  # type: ignore
457        args = dict(
458            oauth_consumer_key=escape.to_basestring(consumer_token["key"]),
459            oauth_token=escape.to_basestring(request_token["key"]),
460            oauth_signature_method="HMAC-SHA1",
461            oauth_timestamp=str(int(time.time())),
462            oauth_nonce=escape.to_basestring(binascii.b2a_hex(uuid.uuid4().bytes)),
463            oauth_version="1.0",
464        )
465        if "verifier" in request_token:
466            args["oauth_verifier"] = request_token["verifier"]
467
468        if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
469            signature = _oauth10a_signature(
470                consumer_token, "GET", url, args, request_token
471            )
472        else:
473            signature = _oauth_signature(
474                consumer_token, "GET", url, args, request_token
475            )
476
477        args["oauth_signature"] = signature
478        return url + "?" + urllib.parse.urlencode(args)
479
480    def _oauth_consumer_token(self) -> Dict[str, Any]:
481        """Subclasses must override this to return their OAuth consumer keys.
482
483        The return value should be a `dict` with keys ``key`` and ``secret``.
484        """
485        raise NotImplementedError()
486
487    async def _oauth_get_user_future(
488        self, access_token: Dict[str, Any]
489    ) -> Dict[str, Any]:
490        """Subclasses must override this to get basic information about the
491        user.
492
493        Should be a coroutine whose result is a dictionary
494        containing information about the user, which may have been
495        retrieved by using ``access_token`` to make a request to the
496        service.
497
498        The access token will be added to the returned dictionary to make
499        the result of `get_authenticated_user`.
500
501        .. versionchanged:: 5.1
502
503           Subclasses may also define this method with ``async def``.
504
505        .. versionchanged:: 6.0
506
507           A synchronous fallback to ``_oauth_get_user`` was removed.
508        """
509        raise NotImplementedError()
510
511    def _oauth_request_parameters(
512        self,
513        url: str,
514        access_token: Dict[str, Any],
515        parameters: Dict[str, Any] = {},
516        method: str = "GET",
517    ) -> Dict[str, Any]:
518        """Returns the OAuth parameters as a dict for the given request.
519
520        parameters should include all POST arguments and query string arguments
521        that will be sent with the request.
522        """
523        consumer_token = self._oauth_consumer_token()
524        base_args = dict(
525            oauth_consumer_key=escape.to_basestring(consumer_token["key"]),
526            oauth_token=escape.to_basestring(access_token["key"]),
527            oauth_signature_method="HMAC-SHA1",
528            oauth_timestamp=str(int(time.time())),
529            oauth_nonce=escape.to_basestring(binascii.b2a_hex(uuid.uuid4().bytes)),
530            oauth_version="1.0",
531        )
532        args = {}
533        args.update(base_args)
534        args.update(parameters)
535        if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
536            signature = _oauth10a_signature(
537                consumer_token, method, url, args, access_token
538            )
539        else:
540            signature = _oauth_signature(
541                consumer_token, method, url, args, access_token
542            )
543        base_args["oauth_signature"] = escape.to_basestring(signature)
544        return base_args
545
546    def get_auth_http_client(self) -> httpclient.AsyncHTTPClient:
547        """Returns the `.AsyncHTTPClient` instance to be used for auth requests.
548
549        May be overridden by subclasses to use an HTTP client other than
550        the default.
551        """
552        return httpclient.AsyncHTTPClient()
553
554
555class OAuth2Mixin:
556    """Abstract implementation of OAuth 2.0.
557
558    See `FacebookGraphMixin` or `GoogleOAuth2Mixin` below for example
559    implementations.
560
561    Class attributes:
562
563    * ``_OAUTH_AUTHORIZE_URL``: The service's authorization url.
564    * ``_OAUTH_ACCESS_TOKEN_URL``:  The service's access token url.
565    """
566
567    def authorize_redirect(
568        self,
569        redirect_uri: Optional[str] = None,
570        client_id: Optional[str] = None,
571        client_secret: Optional[str] = None,
572        extra_params: Optional[Dict[str, Any]] = None,
573        scope: Optional[List[str]] = None,
574        response_type: str = "code",
575    ) -> None:
576        """Redirects the user to obtain OAuth authorization for this service.
577
578        Some providers require that you register a redirect URL with
579        your application instead of passing one via this method. You
580        should call this method to log the user in, and then call
581        ``get_authenticated_user`` in the handler for your
582        redirect URL to complete the authorization process.
583
584        .. versionchanged:: 6.0
585
586           The ``callback`` argument and returned awaitable were removed;
587           this is now an ordinary synchronous function.
588
589        .. deprecated:: 6.4
590           The ``client_secret`` argument (which has never had any effect)
591           is deprecated and will be removed in Tornado 7.0.
592        """
593        if client_secret is not None:
594            warnings.warn("client_secret argument is deprecated", DeprecationWarning)
595        handler = cast(RequestHandler, self)
596        args = {"response_type": response_type}
597        if redirect_uri is not None:
598            args["redirect_uri"] = redirect_uri
599        if client_id is not None:
600            args["client_id"] = client_id
601        if extra_params:
602            args.update(extra_params)
603        if scope:
604            args["scope"] = " ".join(scope)
605        url = self._OAUTH_AUTHORIZE_URL  # type: ignore
606        handler.redirect(url_concat(url, args))
607
608    def _oauth_request_token_url(
609        self,
610        redirect_uri: Optional[str] = None,
611        client_id: Optional[str] = None,
612        client_secret: Optional[str] = None,
613        code: Optional[str] = None,
614        extra_params: Optional[Dict[str, Any]] = None,
615    ) -> str:
616        url = self._OAUTH_ACCESS_TOKEN_URL  # type: ignore
617        args = {}  # type: Dict[str, str]
618        if redirect_uri is not None:
619            args["redirect_uri"] = redirect_uri
620        if code is not None:
621            args["code"] = code
622        if client_id is not None:
623            args["client_id"] = client_id
624        if client_secret is not None:
625            args["client_secret"] = client_secret
626        if extra_params:
627            args.update(extra_params)
628        return url_concat(url, args)
629
630    async def oauth2_request(
631        self,
632        url: str,
633        access_token: Optional[str] = None,
634        post_args: Optional[Dict[str, Any]] = None,
635        **args: Any,
636    ) -> Any:
637        """Fetches the given URL auth an OAuth2 access token.
638
639        If the request is a POST, ``post_args`` should be provided. Query
640        string arguments should be given as keyword arguments.
641
642        Example usage:
643
644        ..testcode::
645
646            class MainHandler(tornado.web.RequestHandler,
647                              tornado.auth.FacebookGraphMixin):
648                @tornado.web.authenticated
649                async def get(self):
650                    new_entry = await self.oauth2_request(
651                        "https://graph.facebook.com/me/feed",
652                        post_args={"message": "I am posting from my Tornado application!"},
653                        access_token=self.current_user["access_token"])
654
655                    if not new_entry:
656                        # Call failed; perhaps missing permission?
657                        self.authorize_redirect()
658                        return
659                    self.finish("Posted a message!")
660
661        .. versionadded:: 4.3
662
663        .. versionchanged::: 6.0
664
665           The ``callback`` argument was removed. Use the returned awaitable object instead.
666        """
667        all_args = {}
668        if access_token:
669            all_args["access_token"] = access_token
670            all_args.update(args)
671
672        if all_args:
673            url += "?" + urllib.parse.urlencode(all_args)
674        http = self.get_auth_http_client()
675        if post_args is not None:
676            response = await http.fetch(
677                url, method="POST", body=urllib.parse.urlencode(post_args)
678            )
679        else:
680            response = await http.fetch(url)
681        return escape.json_decode(response.body)
682
683    def get_auth_http_client(self) -> httpclient.AsyncHTTPClient:
684        """Returns the `.AsyncHTTPClient` instance to be used for auth requests.
685
686        May be overridden by subclasses to use an HTTP client other than
687        the default.
688
689        .. versionadded:: 4.3
690        """
691        return httpclient.AsyncHTTPClient()
692
693
694class TwitterMixin(OAuthMixin):
695    """Twitter OAuth authentication.
696
697    To authenticate with Twitter, register your application with
698    Twitter at http://twitter.com/apps. Then copy your Consumer Key
699    and Consumer Secret to the application
700    `~tornado.web.Application.settings` ``twitter_consumer_key`` and
701    ``twitter_consumer_secret``. Use this mixin on the handler for the
702    URL you registered as your application's callback URL.
703
704    When your application is set up, you can use this mixin like this
705    to authenticate the user with Twitter and get access to their stream:
706
707    .. testcode::
708
709        class TwitterLoginHandler(tornado.web.RequestHandler,
710                                  tornado.auth.TwitterMixin):
711            async def get(self):
712                if self.get_argument("oauth_token", None):
713                    user = await self.get_authenticated_user()
714                    # Save the user using e.g. set_signed_cookie()
715                else:
716                    await self.authorize_redirect()
717
718    The user object returned by `~OAuthMixin.get_authenticated_user`
719    includes the attributes ``username``, ``name``, ``access_token``,
720    and all of the custom Twitter user attributes described at
721    https://dev.twitter.com/docs/api/1.1/get/users/show
722
723    .. deprecated:: 6.3
724       This class refers to version 1.1 of the Twitter API, which has been
725       deprecated by Twitter. Since Twitter has begun to limit access to its
726       API, this class will no longer be updated and will be removed in the
727       future.
728    """
729
730    _OAUTH_REQUEST_TOKEN_URL = "https://api.twitter.com/oauth/request_token"
731    _OAUTH_ACCESS_TOKEN_URL = "https://api.twitter.com/oauth/access_token"
732    _OAUTH_AUTHORIZE_URL = "https://api.twitter.com/oauth/authorize"
733    _OAUTH_AUTHENTICATE_URL = "https://api.twitter.com/oauth/authenticate"
734    _OAUTH_NO_CALLBACKS = False
735    _TWITTER_BASE_URL = "https://api.twitter.com/1.1"
736
737    async def authenticate_redirect(self, callback_uri: Optional[str] = None) -> None:
738        """Just like `~OAuthMixin.authorize_redirect`, but
739        auto-redirects if authorized.
740
741        This is generally the right interface to use if you are using
742        Twitter for single-sign on.
743
744        .. versionchanged:: 3.1
745           Now returns a `.Future` and takes an optional callback, for
746           compatibility with `.gen.coroutine`.
747
748        .. versionchanged:: 6.0
749
750           The ``callback`` argument was removed. Use the returned
751           awaitable object instead.
752        """
753        http = self.get_auth_http_client()
754        response = await http.fetch(
755            self._oauth_request_token_url(callback_uri=callback_uri)
756        )
757        self._on_request_token(self._OAUTH_AUTHENTICATE_URL, None, response)
758
759    async def twitter_request(
760        self,
761        path: str,
762        access_token: Dict[str, Any],
763        post_args: Optional[Dict[str, Any]] = None,
764        **args: Any,
765    ) -> Any:
766        """Fetches the given API path, e.g., ``statuses/user_timeline/btaylor``
767
768        The path should not include the format or API version number.
769        (we automatically use JSON format and API version 1).
770
771        If the request is a POST, ``post_args`` should be provided. Query
772        string arguments should be given as keyword arguments.
773
774        All the Twitter methods are documented at http://dev.twitter.com/
775
776        Many methods require an OAuth access token which you can
777        obtain through `~OAuthMixin.authorize_redirect` and
778        `~OAuthMixin.get_authenticated_user`. The user returned through that
779        process includes an 'access_token' attribute that can be used
780        to make authenticated requests via this method. Example
781        usage:
782
783        .. testcode::
784
785            class MainHandler(tornado.web.RequestHandler,
786                              tornado.auth.TwitterMixin):
787                @tornado.web.authenticated
788                async def get(self):
789                    new_entry = await self.twitter_request(
790                        "/statuses/update",
791                        post_args={"status": "Testing Tornado Web Server"},
792                        access_token=self.current_user["access_token"])
793                    if not new_entry:
794                        # Call failed; perhaps missing permission?
795                        await self.authorize_redirect()
796                        return
797                    self.finish("Posted a message!")
798
799        .. versionchanged:: 6.0
800
801           The ``callback`` argument was removed. Use the returned
802           awaitable object instead.
803        """
804        if path.startswith("http:") or path.startswith("https:"):
805            # Raw urls are useful for e.g. search which doesn't follow the
806            # usual pattern: http://search.twitter.com/search.json
807            url = path
808        else:
809            url = self._TWITTER_BASE_URL + path + ".json"
810        # Add the OAuth resource request signature if we have credentials
811        if access_token:
812            all_args = {}
813            all_args.update(args)
814            all_args.update(post_args or {})
815            method = "POST" if post_args is not None else "GET"
816            oauth = self._oauth_request_parameters(
817                url, access_token, all_args, method=method
818            )
819            args.update(oauth)
820        if args:
821            url += "?" + urllib.parse.urlencode(args)
822        http = self.get_auth_http_client()
823        if post_args is not None:
824            response = await http.fetch(
825                url, method="POST", body=urllib.parse.urlencode(post_args)
826            )
827        else:
828            response = await http.fetch(url)
829        return escape.json_decode(response.body)
830
831    def _oauth_consumer_token(self) -> Dict[str, Any]:
832        handler = cast(RequestHandler, self)
833        handler.require_setting("twitter_consumer_key", "Twitter OAuth")
834        handler.require_setting("twitter_consumer_secret", "Twitter OAuth")
835        return dict(
836            key=handler.settings["twitter_consumer_key"],
837            secret=handler.settings["twitter_consumer_secret"],
838        )
839
840    async def _oauth_get_user_future(
841        self, access_token: Dict[str, Any]
842    ) -> Dict[str, Any]:
843        user = await self.twitter_request(
844            "/account/verify_credentials", access_token=access_token
845        )
846        if user:
847            user["username"] = user["screen_name"]
848        return user
849
850
851class GoogleOAuth2Mixin(OAuth2Mixin):
852    """Google authentication using OAuth2.
853
854    In order to use, register your application with Google and copy the
855    relevant parameters to your application settings.
856
857    * Go to the Google Dev Console at http://console.developers.google.com
858    * Select a project, or create a new one.
859    * Depending on permissions required, you may need to set your app to
860      "testing" mode and add your account as a test user, or go through
861      a verfication process. You may also need to use the "Enable
862      APIs and Services" command to enable specific services.
863    * In the sidebar on the left, select Credentials.
864    * Click CREATE CREDENTIALS and click OAuth client ID.
865    * Under Application type, select Web application.
866    * Name OAuth 2.0 client and click Create.
867    * Copy the "Client secret" and "Client ID" to the application settings as
868      ``{"google_oauth": {"key": CLIENT_ID, "secret": CLIENT_SECRET}}``
869    * You must register the ``redirect_uri`` you plan to use with this class
870      on the Credentials page.
871
872    .. versionadded:: 3.2
873    """
874
875    _OAUTH_AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/v2/auth"
876    _OAUTH_ACCESS_TOKEN_URL = "https://www.googleapis.com/oauth2/v4/token"
877    _OAUTH_USERINFO_URL = "https://www.googleapis.com/oauth2/v1/userinfo"
878    _OAUTH_NO_CALLBACKS = False
879    _OAUTH_SETTINGS_KEY = "google_oauth"
880
881    def get_google_oauth_settings(self) -> Dict[str, str]:
882        """Return the Google OAuth 2.0 credentials that you created with
883        [Google Cloud
884        Platform](https://console.cloud.google.com/apis/credentials). The dict
885        format is::
886
887            {
888                "key": "your_client_id", "secret": "your_client_secret"
889            }
890
891        If your credentials are stored differently (e.g. in a db) you can
892        override this method for custom provision.
893        """
894        handler = cast(RequestHandler, self)
895        return handler.settings[self._OAUTH_SETTINGS_KEY]
896
897    async def get_authenticated_user(
898        self,
899        redirect_uri: str,
900        code: str,
901        client_id: Optional[str] = None,
902        client_secret: Optional[str] = None,
903    ) -> Dict[str, Any]:
904        """Handles the login for the Google user, returning an access token.
905
906        The result is a dictionary containing an ``access_token`` field
907        ([among others](https://developers.google.com/identity/protocols/OAuth2WebServer#handlingtheresponse)).
908        Unlike other ``get_authenticated_user`` methods in this package,
909        this method does not return any additional information about the user.
910        The returned access token can be used with `OAuth2Mixin.oauth2_request`
911        to request additional information (perhaps from
912        ``https://www.googleapis.com/oauth2/v2/userinfo``)
913
914        Example usage:
915
916        .. testsetup::
917
918            import urllib
919
920        .. testcode::
921
922            class GoogleOAuth2LoginHandler(tornado.web.RequestHandler,
923                                           tornado.auth.GoogleOAuth2Mixin):
924                async def get(self):
925                    # Google requires an exact match for redirect_uri, so it's
926                    # best to get it from your app configuration instead of from
927                    # self.request.full_uri().
928                    redirect_uri = urllib.parse.urljoin(self.application.settings['redirect_base_uri'],
929                        self.reverse_url('google_oauth'))
930                    async def get(self):
931                        if self.get_argument('code', False):
932                            access = await self.get_authenticated_user(
933                                redirect_uri=redirect_uri,
934                                code=self.get_argument('code'))
935                            user = await self.oauth2_request(
936                                "https://www.googleapis.com/oauth2/v1/userinfo",
937                                access_token=access["access_token"])
938                            # Save the user and access token. For example:
939                            user_cookie = dict(id=user["id"], access_token=access["access_token"])
940                            self.set_signed_cookie("user", json.dumps(user_cookie))
941                            self.redirect("/")
942                        else:
943                            self.authorize_redirect(
944                                redirect_uri=redirect_uri,
945                                client_id=self.get_google_oauth_settings()['key'],
946                                scope=['profile', 'email'],
947                                response_type='code',
948                                extra_params={'approval_prompt': 'auto'})
949
950        .. versionchanged:: 6.0
951
952           The ``callback`` argument was removed. Use the returned awaitable object instead.
953        """  # noqa: E501
954
955        if client_id is None or client_secret is None:
956            settings = self.get_google_oauth_settings()
957            if client_id is None:
958                client_id = settings["key"]
959            if client_secret is None:
960                client_secret = settings["secret"]
961        http = self.get_auth_http_client()
962        body = urllib.parse.urlencode(
963            {
964                "redirect_uri": redirect_uri,
965                "code": code,
966                "client_id": client_id,
967                "client_secret": client_secret,
968                "grant_type": "authorization_code",
969            }
970        )
971
972        response = await http.fetch(
973            self._OAUTH_ACCESS_TOKEN_URL,
974            method="POST",
975            headers={"Content-Type": "application/x-www-form-urlencoded"},
976            body=body,
977        )
978        return escape.json_decode(response.body)
979
980
981class FacebookGraphMixin(OAuth2Mixin):
982    """Facebook authentication using the new Graph API and OAuth2."""
983
984    _OAUTH_ACCESS_TOKEN_URL = "https://graph.facebook.com/oauth/access_token?"
985    _OAUTH_AUTHORIZE_URL = "https://www.facebook.com/dialog/oauth?"
986    _OAUTH_NO_CALLBACKS = False
987    _FACEBOOK_BASE_URL = "https://graph.facebook.com"
988
989    async def get_authenticated_user(
990        self,
991        redirect_uri: str,
992        client_id: str,
993        client_secret: str,
994        code: str,
995        extra_fields: Optional[Dict[str, Any]] = None,
996    ) -> Optional[Dict[str, Any]]:
997        """Handles the login for the Facebook user, returning a user object.
998
999        Example usage:
1000
1001        .. testcode::
1002
1003            class FacebookGraphLoginHandler(tornado.web.RequestHandler,
1004                                            tornado.auth.FacebookGraphMixin):
1005              async def get(self):
1006                redirect_uri = urllib.parse.urljoin(
1007                    self.application.settings['redirect_base_uri'],
1008                    self.reverse_url('facebook_oauth'))
1009                if self.get_argument("code", False):
1010                    user = await self.get_authenticated_user(
1011                        redirect_uri=redirect_uri,
1012                        client_id=self.settings["facebook_api_key"],
1013                        client_secret=self.settings["facebook_secret"],
1014                        code=self.get_argument("code"))
1015                    # Save the user with e.g. set_signed_cookie
1016                else:
1017                    self.authorize_redirect(
1018                        redirect_uri=redirect_uri,
1019                        client_id=self.settings["facebook_api_key"],
1020                        extra_params={"scope": "user_posts"})
1021
1022        This method returns a dictionary which may contain the following fields:
1023
1024        * ``access_token``, a string which may be passed to `facebook_request`
1025        * ``session_expires``, an integer encoded as a string representing
1026          the time until the access token expires in seconds. This field should
1027          be used like ``int(user['session_expires'])``; in a future version of
1028          Tornado it will change from a string to an integer.
1029        * ``id``, ``name``, ``first_name``, ``last_name``, ``locale``, ``picture``,
1030          ``link``, plus any fields named in the ``extra_fields`` argument. These
1031          fields are copied from the Facebook graph API
1032          `user object <https://developers.facebook.com/docs/graph-api/reference/user>`_
1033
1034        .. versionchanged:: 4.5
1035           The ``session_expires`` field was updated to support changes made to the
1036           Facebook API in March 2017.
1037
1038        .. versionchanged:: 6.0
1039
1040           The ``callback`` argument was removed. Use the returned awaitable object instead.
1041        """
1042        http = self.get_auth_http_client()
1043        args = {
1044            "redirect_uri": redirect_uri,
1045            "code": code,
1046            "client_id": client_id,
1047            "client_secret": client_secret,
1048        }
1049
1050        fields = {"id", "name", "first_name", "last_name", "locale", "picture", "link"}
1051        if extra_fields:
1052            fields.update(extra_fields)
1053
1054        response = await http.fetch(
1055            self._oauth_request_token_url(**args)  # type: ignore
1056        )
1057        args = escape.json_decode(response.body)
1058        session = {
1059            "access_token": args.get("access_token"),
1060            "expires_in": args.get("expires_in"),
1061        }
1062        assert session["access_token"] is not None
1063
1064        user = await self.facebook_request(
1065            path="/me",
1066            access_token=session["access_token"],
1067            appsecret_proof=hmac.new(
1068                key=client_secret.encode("utf8"),
1069                msg=session["access_token"].encode("utf8"),
1070                digestmod=hashlib.sha256,
1071            ).hexdigest(),
1072            fields=",".join(fields),
1073        )
1074
1075        if user is None:
1076            return None
1077
1078        fieldmap = {}
1079        for field in fields:
1080            fieldmap[field] = user.get(field)
1081
1082        # session_expires is converted to str for compatibility with
1083        # older versions in which the server used url-encoding and
1084        # this code simply returned the string verbatim.
1085        # This should change in Tornado 5.0.
1086        fieldmap.update(
1087            {
1088                "access_token": session["access_token"],
1089                "session_expires": str(session.get("expires_in")),
1090            }
1091        )
1092        return fieldmap
1093
1094    async def facebook_request(
1095        self,
1096        path: str,
1097        access_token: Optional[str] = None,
1098        post_args: Optional[Dict[str, Any]] = None,
1099        **args: Any,
1100    ) -> Any:
1101        """Fetches the given relative API path, e.g., "/btaylor/picture"
1102
1103        If the request is a POST, ``post_args`` should be provided. Query
1104        string arguments should be given as keyword arguments.
1105
1106        An introduction to the Facebook Graph API can be found at
1107        http://developers.facebook.com/docs/api
1108
1109        Many methods require an OAuth access token which you can
1110        obtain through `~OAuth2Mixin.authorize_redirect` and
1111        `get_authenticated_user`. The user returned through that
1112        process includes an ``access_token`` attribute that can be
1113        used to make authenticated requests via this method.
1114
1115        Example usage:
1116
1117        .. testcode::
1118
1119            class MainHandler(tornado.web.RequestHandler,
1120                              tornado.auth.FacebookGraphMixin):
1121                @tornado.web.authenticated
1122                async def get(self):
1123                    new_entry = await self.facebook_request(
1124                        "/me/feed",
1125                        post_args={"message": "I am posting from my Tornado application!"},
1126                        access_token=self.current_user["access_token"])
1127
1128                    if not new_entry:
1129                        # Call failed; perhaps missing permission?
1130                        self.authorize_redirect()
1131                        return
1132                    self.finish("Posted a message!")
1133
1134        The given path is relative to ``self._FACEBOOK_BASE_URL``,
1135        by default "https://graph.facebook.com".
1136
1137        This method is a wrapper around `OAuth2Mixin.oauth2_request`;
1138        the only difference is that this method takes a relative path,
1139        while ``oauth2_request`` takes a complete url.
1140
1141        .. versionchanged:: 3.1
1142           Added the ability to override ``self._FACEBOOK_BASE_URL``.
1143
1144        .. versionchanged:: 6.0
1145
1146           The ``callback`` argument was removed. Use the returned awaitable object instead.
1147        """
1148        url = self._FACEBOOK_BASE_URL + path
1149        return await self.oauth2_request(
1150            url, access_token=access_token, post_args=post_args, **args
1151        )
1152
1153
1154def _oauth_signature(
1155    consumer_token: Dict[str, Any],
1156    method: str,
1157    url: str,
1158    parameters: Dict[str, Any] = {},
1159    token: Optional[Dict[str, Any]] = None,
1160) -> bytes:
1161    """Calculates the HMAC-SHA1 OAuth signature for the given request.
1162
1163    See http://oauth.net/core/1.0/#signing_process
1164    """
1165    parts = urllib.parse.urlparse(url)
1166    scheme, netloc, path = parts[:3]
1167    normalized_url = scheme.lower() + "://" + netloc.lower() + path
1168
1169    base_elems = []
1170    base_elems.append(method.upper())
1171    base_elems.append(normalized_url)
1172    base_elems.append(
1173        "&".join(f"{k}={_oauth_escape(str(v))}" for k, v in sorted(parameters.items()))
1174    )
1175    base_string = "&".join(_oauth_escape(e) for e in base_elems)
1176
1177    key_elems = [escape.utf8(consumer_token["secret"])]
1178    key_elems.append(escape.utf8(token["secret"] if token else ""))
1179    key = b"&".join(key_elems)
1180
1181    hash = hmac.new(key, escape.utf8(base_string), hashlib.sha1)
1182    return binascii.b2a_base64(hash.digest())[:-1]
1183
1184
1185def _oauth10a_signature(
1186    consumer_token: Dict[str, Any],
1187    method: str,
1188    url: str,
1189    parameters: Dict[str, Any] = {},
1190    token: Optional[Dict[str, Any]] = None,
1191) -> bytes:
1192    """Calculates the HMAC-SHA1 OAuth 1.0a signature for the given request.
1193
1194    See http://oauth.net/core/1.0a/#signing_process
1195    """
1196    parts = urllib.parse.urlparse(url)
1197    scheme, netloc, path = parts[:3]
1198    normalized_url = scheme.lower() + "://" + netloc.lower() + path
1199
1200    base_elems = []

Showing the first 1,200 of 1236 lines. Download the file for the rest.

codekingpro/portable-devtools · Team Ai