294 lines
11 KiB
ReStructuredText
294 lines
11 KiB
ReStructuredText
Authentication and security
|
|
===========================
|
|
|
|
.. testsetup::
|
|
|
|
import tornado.web
|
|
|
|
Cookies and secure cookies
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
You can set cookies in the user's browser with the ``set_cookie``
|
|
method:
|
|
|
|
.. testcode::
|
|
|
|
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!")
|
|
|
|
.. testoutput::
|
|
:hide:
|
|
|
|
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 `~.RequestHandler.set_secure_cookie` and
|
|
`~.RequestHandler.get_secure_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:
|
|
|
|
.. testcode::
|
|
|
|
application = tornado.web.Application([
|
|
(r"/", MainHandler),
|
|
], cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__")
|
|
|
|
.. testoutput::
|
|
:hide:
|
|
|
|
Signed cookies contain the encoded value of the cookie in addition to a
|
|
timestamp and an `HMAC <http://en.wikipedia.org/wiki/HMAC>`_ signature.
|
|
If the cookie is old or if the signature doesn't match,
|
|
``get_secure_cookie`` will return ``None`` just as if the cookie isn't
|
|
set. The secure version of the example above:
|
|
|
|
.. testcode::
|
|
|
|
class MainHandler(tornado.web.RequestHandler):
|
|
def get(self):
|
|
if not self.get_secure_cookie("mycookie"):
|
|
self.set_secure_cookie("mycookie", "myvalue")
|
|
self.write("Your cookie was not set yet!")
|
|
else:
|
|
self.write("Your cookie was set!")
|
|
|
|
.. testoutput::
|
|
:hide:
|
|
|
|
Tornado's secure 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 secure cookies expire after 30 days. To change this,
|
|
use the ``expires_days`` keyword argument to ``set_secure_cookie`` *and* the
|
|
``max_age_days`` argument to ``get_secure_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 `~.RequestHandler.get_secure_cookie_key_version`.
|
|
|
|
.. _user-authentication:
|
|
|
|
User authentication
|
|
~~~~~~~~~~~~~~~~~~~
|
|
|
|
The currently authenticated user is available in every request handler
|
|
as `self.current_user <.RequestHandler.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:
|
|
|
|
.. testcode::
|
|
|
|
class BaseHandler(tornado.web.RequestHandler):
|
|
def get_current_user(self):
|
|
return self.get_secure_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_secure_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__")
|
|
|
|
.. testoutput::
|
|
:hide:
|
|
|
|
You can require that the user be logged in using the `Python
|
|
decorator <http://www.python.org/dev/peps/pep-0318/>`_
|
|
`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:
|
|
|
|
.. testcode::
|
|
|
|
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)
|
|
|
|
.. testoutput::
|
|
:hide:
|
|
|
|
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
|
|
<https://github.com/tornadoweb/tornado/tree/stable/demos/blog>`_ for a
|
|
complete example that uses authentication (and stores user data in a
|
|
MySQL 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:
|
|
|
|
.. testcode::
|
|
|
|
class GoogleOAuth2LoginHandler(tornado.web.RequestHandler,
|
|
tornado.auth.GoogleOAuth2Mixin):
|
|
@tornado.gen.coroutine
|
|
def get(self):
|
|
if self.get_argument('code', False):
|
|
user = yield self.get_authenticated_user(
|
|
redirect_uri='http://your.site.com/auth/google',
|
|
code=self.get_argument('code'))
|
|
# Save the user with e.g. set_secure_cookie
|
|
else:
|
|
yield 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'})
|
|
|
|
.. testoutput::
|
|
:hide:
|
|
|
|
See the `tornado.auth` module documentation for more details.
|
|
|
|
.. _xsrf:
|
|
|
|
Cross-site request forgery protection
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
`Cross-site request
|
|
forgery <http://en.wikipedia.org/wiki/Cross-site_request_forgery>`_, or
|
|
XSRF, is a common problem for personalized web applications. See the
|
|
`Wikipedia
|
|
article <http://en.wikipedia.org/wiki/Cross-site_request_forgery>`_ for
|
|
more information on how XSRF works.
|
|
|
|
The generally accepted solution to prevent XSRF is to cookie every user
|
|
with an unpredictable value and include that value as an additional
|
|
argument with every form submission on your site. If the cookie and the
|
|
value in the form submission do not match, then the request is likely
|
|
forged.
|
|
|
|
Tornado comes with built-in XSRF protection. To include it in your site,
|
|
include the application setting ``xsrf_cookies``:
|
|
|
|
.. testcode::
|
|
|
|
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)
|
|
|
|
.. testoutput::
|
|
:hide:
|
|
|
|
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 <http://jquery.com/>`_ 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.
|