Authentication and security

Cookies and signed cookies

You can set cookies in the user’s browser with the set_cookie method:

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        if not self.get_cookie("mycookie"):
            self.set_cookie("mycookie", "myvalue")
            self.write("Your cookie was not set yet!")
        else:
            self.write("Your cookie was set!")

Cookies are not secure and can easily be modified by clients. If you need to set cookies to, e.g., identify the currently logged in user, you need to sign your cookies to prevent forgery. Tornado supports signed cookies with the set_signed_cookie and get_signed_cookie methods. To use these methods, you need to specify a secret key named cookie_secret when you create your application. You can pass in application settings as keyword arguments to your application:

application = tornado.web.Application([
    (r"/", MainHandler),
], cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__")

Signed cookies contain the encoded value of the cookie in addition to a timestamp and an HMAC signature. If the cookie is old or if the signature doesn’t match, get_signed_cookie will return None just as if the cookie isn’t set. The secure version of the example above:

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        if not self.get_signed_cookie("mycookie"):
            self.set_signed_cookie("mycookie", "myvalue")
            self.write("Your cookie was not set yet!")
        else:
            self.write("Your cookie was set!")

Tornado’s signed cookies guarantee integrity but not confidentiality. That is, the cookie cannot be modified but its contents can be seen by the user. The cookie_secret is a symmetric key and must be kept secret – anyone who obtains the value of this key could produce their own signed cookies.

By default, Tornado’s signed cookies expire after 30 days. To change this, use the expires_days keyword argument to set_signed_cookie and the max_age_days argument to get_signed_cookie. These two values are passed separately so that you may e.g. have a cookie that is valid for 30 days for most purposes, but for certain sensitive actions (such as changing billing information) you use a smaller max_age_days when reading the cookie.

Tornado also supports multiple signing keys to enable signing key rotation. cookie_secret then must be a dict with integer key versions as keys and the corresponding secrets as values. The currently used signing key must then be set as key_version application setting but all other keys in the dict are allowed for cookie signature validation, if the correct key version is set in the cookie. To implement cookie updates, the current signing key version can be queried via get_signed_cookie_key_version.

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.

XSRF cookies

Tornado supports a second, older method of preventing XSRF attacks using cookies. This involves modifying every form submission on your site to include a value that can be compared with a cookie set for this purpose. This is an intrusive modification and is no longer recommended for new applications, although it was the only option available in Tornado before version 6.6.

To use cookie-based XSRF protection, set the application setting xsrf_cookies to True, and modify your application as described below. The xsrf_cookies setting will emit deprecation warnings beginning in Tornado 6.7 and may be removed in future versions (Tornado 7.0 at the earliest).

settings = {
    "cookie_secret": "__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__",
    "login_url": "/login",
    "xsrf_cookies": True,
}
application = tornado.web.Application([
    (r"/", MainHandler),
    (r"/login", LoginHandler),
], **settings)

If xsrf_cookies is set, the Tornado web application will set the _xsrf cookie for all users and reject all POST, PUT, and DELETE requests that do not contain a correct _xsrf value. If you turn this setting on, you need to instrument all forms that submit via POST to contain this field. You can do this with the special UIModule xsrf_form_html(), available in all templates:

<form action="/new_message" method="post">
  {% module xsrf_form_html() %}
  <input type="text" name="message"/>
  <input type="submit" value="Post"/>
</form>

If you submit AJAX POST requests, you will also need to instrument your JavaScript to include the _xsrf value with each request. This is the jQuery function we use at FriendFeed for AJAX POST requests that automatically adds the _xsrf value to all requests:

function getCookie(name) {
    var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
    return r ? r[1] : undefined;
}

jQuery.postJSON = function(url, args, callback) {
    args._xsrf = getCookie("_xsrf");
    $.ajax({url: url, data: $.param(args), dataType: "text", type: "POST",
        success: function(response) {
        callback(eval("(" + response + ")"));
    }});
};

For PUT and DELETE requests (as well as POST requests that do not use form-encoded arguments), the XSRF token may also be passed via an HTTP header named X-XSRFToken. The XSRF cookie is normally set when xsrf_form_html is used, but in a pure-JavaScript application that does not use any regular forms you may need to access self.xsrf_token manually (just reading the property is enough to set the cookie as a side effect).

If you need to customize XSRF behavior on a per-handler basis, you can override RequestHandler.check_xsrf_cookie(). For example, if you have an API whose authentication does not use cookies, you may want to disable XSRF protection by making check_xsrf_cookie() do nothing. However, if you support both cookie and non-cookie-based authentication, it is important that XSRF protection be used whenever the current request is authenticated with a cookie.

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.