Authentication and security
User authentication
The currently authenticated user is available in every request handler
as self.current_user, and in every
template as current_user. By default, current_user is
None.
To implement user authentication in your application, you need to
override the get_current_user() method in your request handlers to
determine the current user based on, e.g., the value of a cookie. Here
is an example that lets users log into the application simply by
specifying a nickname, which is then saved in a cookie:
class BaseHandler(tornado.web.RequestHandler):
def get_current_user(self):
return self.get_signed_cookie("user")
class MainHandler(BaseHandler):
def get(self):
if not self.current_user:
self.redirect("/login")
return
name = tornado.escape.xhtml_escape(self.current_user)
self.write("Hello, " + name)
class LoginHandler(BaseHandler):
def get(self):
self.write('<html><body><form action="/login" method="post">'
'Name: <input type="text" name="name">'
'<input type="submit" value="Sign in">'
'</form></body></html>')
def post(self):
self.set_signed_cookie("user", self.get_argument("name"))
self.redirect("/")
application = tornado.web.Application([
(r"/", MainHandler),
(r"/login", LoginHandler),
], cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__")
You can require that the user be logged in using the Python
decorator
tornado.web.authenticated. If a request goes to a method with this
decorator, and the user is not logged in, they will be redirected to
login_url (another application setting). The example above could be
rewritten:
class MainHandler(BaseHandler):
@tornado.web.authenticated
def get(self):
name = tornado.escape.xhtml_escape(self.current_user)
self.write("Hello, " + name)
settings = {
"cookie_secret": "__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__",
"login_url": "/login",
}
application = tornado.web.Application([
(r"/", MainHandler),
(r"/login", LoginHandler),
], **settings)
If you decorate post() methods with the authenticated
decorator, and the user is not logged in, the server will send a
403 response. The @authenticated decorator is simply
shorthand for if not self.current_user: self.redirect() and may
not be appropriate for non-browser-based login schemes.
Check out the Tornado Blog example application for a complete example that uses authentication (and stores user data in a PostgreSQL database).
Third party authentication
The tornado.auth module implements the authentication and
authorization protocols for a number of the most popular sites on the
web, including Google/Gmail, Facebook, Twitter, and FriendFeed.
The module includes methods to log users in via these sites and, where
applicable, methods to authorize access to the service so you can, e.g.,
download a user’s address book or publish a Twitter message on their
behalf.
Here is an example handler that uses Google for authentication, saving the Google credentials in a cookie for later access:
class GoogleOAuth2LoginHandler(tornado.web.RequestHandler,
tornado.auth.GoogleOAuth2Mixin):
async def get(self):
if self.get_argument('code', False):
user = await self.get_authenticated_user(
redirect_uri='http://your.site.com/auth/google',
code=self.get_argument('code'))
# Save the user with e.g. set_signed_cookie
else:
await self.authorize_redirect(
redirect_uri='http://your.site.com/auth/google',
client_id=self.settings['google_oauth']['key'],
scope=['profile', 'email'],
response_type='code',
extra_params={'approval_prompt': 'auto'})
See the tornado.auth module documentation for more details.
Cross-origin protection
Cross-origin protection guards against a class of attacks most commonly known as cross-site request forgery (XSRF or CSRF). This is a common problem for personalized web applications, in which a third-party site can construct a request which a visitor’s browser will submit to your application along with the visitor’s authentication cookies.
Modern browsers send a header which the server can use to identify and block
cross-site requests. Tornado from version 6.6 can check this for you, if you
enable the application setting cross_origin_protection:
settings = {
"login_url": "/login",
"cross_origin_protection": True,
}
application = tornado.web.Application([
(r"/", MainHandler),
(r"/login", LoginHandler),
], **settings)
Note
This feature is a new security measure in the web stack. Tornado uses
the same terminology as Go’s cross-origin protection, with an algorithm based
on this blog post by Filippo Valsorda. Other
packages and documents refer to it by different names, such as “fetch metadata”
or Sec-Fetch-Site.
Conversely, this feature is unrelated to Cross-Origin Resource Sharing (CORS)
and the Access-Control-Allow-Origin header. Applications using
cross_origin_protection should still consider their CORS requirements.
With this setting, the application will reject state-changing requests if the
Sec-Fetch-Site
header is set to something other than same-origin or none. If that
header is missing, it rejects these requests if the Origin and Host
headers do not match. You can allow specific additional origins with the
trusted_origins application setting (a list).
The cross_origin_protection setting was introduced in Tornado 6.6 and defaults
to False. It is expected that in Tornado 6.7 it will be enabled by default.
Note
These checks apply to HTTP verbs like POST or PUT, which typically
modify application state. GET, HEAD and OPTIONS requests should
not affect application state, and are not checked on the server, but the
browser can still keep the response from leaking.
The exception is WebSocket connections, which begin with a GET request
but are not subject to the browser’s same-origin policy. WebSocketHandler
always performs these checks, whether or not cross_origin_protection
is enabled. Instead of RequestHandler.check_trusted_origin, it calls
WebSocketHandler.check_origin (after prepare), whose
default implementation calls check_trusted_origin and therefore also
uses the trusted_origins setting.
This protection is more convenient than older methods like XSRF cookies, described below, because it does not require any modifications to the HTML or JavaScript making the requests. It should be sufficient to protect most web applications from XSRF attacks.
Some applications may need to customize the trusted_origins application setting (or
override RequestHandler.check_trusted_origin) to include additional domains that
should be allowed to send cross-site requests. Note that an origin looks like the
beginning of a url but never includes a trailing slash:
"trusted_origins": [
"https://example.com",
"https://example.org",
],
An “origin” is a smaller scope than a “site” (a site includes subdomains). This means that
applications built for a site-based security model may need to include their subdomains as
“trusted origins” to transition to cross_origin_protection. Wildcards are not supported in
the trusted_origins application setting but can be implemented by overriding
RequestHandler.check_trusted_origin.
RequestHandler.check_trusted_origin is called before prepare,
so it cannot depend on how (or whether) the request is authenticated. This is
intentional: cookies are attached to cross-site requests automatically, so the
origin must be checked before the request is allowed to act on its cookies.
This protection relies on the Sec-Fetch-Site HTTP header, and, as a fallback for
when that header is not present, the Origin and Host headers. Any proxies
or routers in your application serving stack must be configured to pass these headers
through. Note that as XSRF is fundamentally a browser-based attack, requests from
non-browser clients (which do not send Sec-Fetch-Site or Origin) are always
permitted. This means that if a proxy strips both of these headers, or a very old
browser does not send them, the protection is silently disabled.
All major browsers have supported the Sec-Fetch-Site header since 2023, although
this header is only sent on HTTPS connections or to localhost. For older browsers or
unencrypted applications, we fall back to comparing the Origin and Host
headers.
DNS Rebinding
DNS rebinding is an
attack that can bypass the same-origin policy and allow external sites
to access resources on private networks. This attack involves a DNS
name (with a short TTL) that alternates between returning an IP
address controlled by the attacker and one controlled by the victim
(often a guessable private IP address such as 127.0.0.1 or
192.168.1.1).
Applications that use TLS are not vulnerable to this attack (because the browser will display certificate mismatch warnings that block automated access to the target site).
Applications that cannot use TLS and rely on network-level access
controls (for example, assuming that a server on 127.0.0.1 can
only be accessed by the local machine) should guard against DNS
rebinding by validating the Host HTTP header. This means passing a
restrictive hostname pattern to either a HostMatches router or the
first argument of Application.add_handlers:
# BAD: uses a default host pattern of r'.*'
app = Application([('/foo', FooHandler)])
# GOOD: only matches localhost or its ip address.
app = Application()
app.add_handlers(r'(localhost|127\.0\.0\.1)',
[('/foo', FooHandler)])
# GOOD: same as previous example using tornado.routing.
app = Application([
(HostMatches(r'(localhost|127\.0\.0\.1)'),
[('/foo', FooHandler)]),
])
In addition, the default_host argument to Application and the
DefaultHostMatches router must not be used in applications that may
be vulnerable to DNS rebinding, because it has a similar effect to a
wildcard host pattern.