Skip to content

Commit

Permalink
Merge pull request #3224 from bdarnell/cookie-updates
Browse files Browse the repository at this point in the history
web: Cookie updates
  • Loading branch information
bdarnell committed Jan 30, 2023
2 parents 5953601 + 91a54dd commit 0f8e10b
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 100 deletions.
6 changes: 3 additions & 3 deletions demos/blog/blog.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ async def queryone(self, stmt, *args):
async def prepare(self):
# get_current_user cannot be a coroutine, so set
# self.current_user in prepare instead.
user_id = self.get_secure_cookie("blogdemo_user")
user_id = self.get_signed_cookie("blogdemo_user")
if user_id:
self.current_user = await self.queryone(
"SELECT * FROM authors WHERE id = %s", int(user_id)
Expand Down Expand Up @@ -242,7 +242,7 @@ async def post(self):
self.get_argument("name"),
tornado.escape.to_unicode(hashed_password),
)
self.set_secure_cookie("blogdemo_user", str(author.id))
self.set_signed_cookie("blogdemo_user", str(author.id))
self.redirect(self.get_argument("next", "/"))


Expand All @@ -269,7 +269,7 @@ async def post(self):
tornado.escape.utf8(author.hashed_password),
)
if password_equal:
self.set_secure_cookie("blogdemo_user", str(author.id))
self.set_signed_cookie("blogdemo_user", str(author.id))
self.redirect(self.get_argument("next", "/"))
else:
self.render("login.html", error="incorrect password")
Expand Down
4 changes: 2 additions & 2 deletions demos/facebook/facebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def __init__(self):

class BaseHandler(tornado.web.RequestHandler):
def get_current_user(self):
user_json = self.get_secure_cookie("fbdemo_user")
user_json = self.get_signed_cookie("fbdemo_user")
if not user_json:
return None
return tornado.escape.json_decode(user_json)
Expand Down Expand Up @@ -84,7 +84,7 @@ async def get(self):
client_secret=self.settings["facebook_secret"],
code=self.get_argument("code"),
)
self.set_secure_cookie("fbdemo_user", tornado.escape.json_encode(user))
self.set_signed_cookie("fbdemo_user", tornado.escape.json_encode(user))
self.redirect(self.get_argument("next", "/"))
return
self.authorize_redirect(
Expand Down
4 changes: 2 additions & 2 deletions demos/twitter/twitterdemo.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class BaseHandler(RequestHandler):
COOKIE_NAME = "twitterdemo_user"

def get_current_user(self):
user_json = self.get_secure_cookie(self.COOKIE_NAME)
user_json = self.get_signed_cookie(self.COOKIE_NAME)
if not user_json:
return None
return json_decode(user_json)
Expand All @@ -75,7 +75,7 @@ def get(self):
if self.get_argument("oauth_token", None):
user = yield self.get_authenticated_user()
del user["description"]
self.set_secure_cookie(self.COOKIE_NAME, json_encode(user))
self.set_signed_cookie(self.COOKIE_NAME, json_encode(user))
self.redirect(self.get_argument("next", "/"))
else:
yield self.authorize_redirect(callback_uri=self.request.full_url())
Expand Down
28 changes: 14 additions & 14 deletions docs/guide/security.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Authentication and security

import tornado

Cookies and secure cookies
Cookies and signed cookies
~~~~~~~~~~~~~~~~~~~~~~~~~~

You can set cookies in the user's browser with the ``set_cookie``
Expand All @@ -27,8 +27,8 @@ method:
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,
signed cookies with the `~.RequestHandler.set_signed_cookie` and
`~.RequestHandler.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:
Expand All @@ -45,31 +45,31 @@ keyword arguments to your application:
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
``get_signed_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")
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!")

.. testoutput::
:hide:

Tornado's secure cookies guarantee integrity but not confidentiality.
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 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
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.
Expand All @@ -81,7 +81,7 @@ 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`.
queried via `~.RequestHandler.get_signed_cookie_key_version`.

.. _user-authentication:

Expand All @@ -103,7 +103,7 @@ specifying a nickname, which is then saved in a cookie:

class BaseHandler(tornado.web.RequestHandler):
def get_current_user(self):
return self.get_secure_cookie("user")
return self.get_signed_cookie("user")

class MainHandler(BaseHandler):
def get(self):
Expand All @@ -121,7 +121,7 @@ specifying a nickname, which is then saved in a cookie:
'</form></body></html>')

def post(self):
self.set_secure_cookie("user", self.get_argument("name"))
self.set_signed_cookie("user", self.get_argument("name"))
self.redirect("/")

application = tornado.web.Application([
Expand Down Expand Up @@ -193,7 +193,7 @@ the Google credentials in a cookie for later access:
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_secure_cookie
# Save the user with e.g. set_signed_cookie
else:
await self.authorize_redirect(
redirect_uri='http://your.site.com/auth/google',
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/templates.rst
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ by overriding `.RequestHandler.get_user_locale`:

class BaseHandler(tornado.web.RequestHandler):
def get_current_user(self):
user_id = self.get_secure_cookie("user")
user_id = self.get_signed_cookie("user")
if not user_id: return None
return self.backend.get_user_by_id(user_id)

Expand Down
30 changes: 24 additions & 6 deletions docs/web.rst
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,27 @@
.. automethod:: RequestHandler.set_cookie
.. automethod:: RequestHandler.clear_cookie
.. automethod:: RequestHandler.clear_all_cookies
.. automethod:: RequestHandler.get_secure_cookie
.. automethod:: RequestHandler.get_secure_cookie_key_version
.. automethod:: RequestHandler.set_secure_cookie
.. automethod:: RequestHandler.get_signed_cookie
.. automethod:: RequestHandler.get_signed_cookie_key_version
.. automethod:: RequestHandler.set_signed_cookie
.. method:: RequestHandler.get_secure_cookie

Deprecated alias for ``get_signed_cookie``.

.. deprecated:: 6.3

.. method:: RequestHandler.get_secure_cookie_key_version

Deprecated alias for ``get_signed_cookie_key_version``.

.. deprecated:: 6.3

.. method:: RequestHandler.set_secure_cookie

Deprecated alias for ``set_signed_cookie``.

.. deprecated:: 6.3

.. automethod:: RequestHandler.create_signed_value
.. autodata:: MIN_SUPPORTED_SIGNED_VALUE_VERSION
.. autodata:: MAX_SUPPORTED_SIGNED_VALUE_VERSION
Expand Down Expand Up @@ -217,9 +235,9 @@

Authentication and security settings:

* ``cookie_secret``: Used by `RequestHandler.get_secure_cookie`
and `.set_secure_cookie` to sign cookies.
* ``key_version``: Used by requestHandler `.set_secure_cookie`
* ``cookie_secret``: Used by `RequestHandler.get_signed_cookie`
and `.set_signed_cookie` to sign cookies.
* ``key_version``: Used by requestHandler `.set_signed_cookie`
to sign cookies with a specific key when ``cookie_secret``
is a key dictionary.
* ``login_url``: The `authenticated` decorator will redirect
Expand Down
8 changes: 4 additions & 4 deletions tornado/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ async def get(self):
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_secure_cookie
# Save the user with e.g. set_signed_cookie
else:
self.authorize_redirect(
redirect_uri='http://your.site.com/auth/google',
Expand Down Expand Up @@ -694,7 +694,7 @@ class TwitterLoginHandler(tornado.web.RequestHandler,
async def get(self):
if self.get_argument("oauth_token", None):
user = await self.get_authenticated_user()
# Save the user using e.g. set_secure_cookie()
# Save the user using e.g. set_signed_cookie()
else:
await self.authorize_redirect()
Expand Down Expand Up @@ -903,7 +903,7 @@ async def get(self):
"https://www.googleapis.com/oauth2/v1/userinfo",
access_token=access["access_token"])
# Save the user and access token with
# e.g. set_secure_cookie.
# e.g. set_signed_cookie.
else:
self.authorize_redirect(
redirect_uri='http://your.site.com/auth/google',
Expand Down Expand Up @@ -977,7 +977,7 @@ async def get(self):
client_id=self.settings["facebook_api_key"],
client_secret=self.settings["facebook_secret"],
code=self.get_argument("code"))
# Save the user with e.g. set_secure_cookie
# Save the user with e.g. set_signed_cookie
else:
self.authorize_redirect(
redirect_uri='/auth/facebookgraph/',
Expand Down
36 changes: 18 additions & 18 deletions tornado/test/web_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def get(self):


class CookieTestRequestHandler(RequestHandler):
# stub out enough methods to make the secure_cookie functions work
# stub out enough methods to make the signed_cookie functions work
def __init__(self, cookie_secret="0123456789", key_version=None):
# don't call super.__init__
self._cookies = {} # type: typing.Dict[str, bytes]
Expand All @@ -121,13 +121,13 @@ def set_cookie(self, name, value, expires_days=None):
class SecureCookieV1Test(unittest.TestCase):
def test_round_trip(self):
handler = CookieTestRequestHandler()
handler.set_secure_cookie("foo", b"bar", version=1)
self.assertEqual(handler.get_secure_cookie("foo", min_version=1), b"bar")
handler.set_signed_cookie("foo", b"bar", version=1)
self.assertEqual(handler.get_signed_cookie("foo", min_version=1), b"bar")

def test_cookie_tampering_future_timestamp(self):
handler = CookieTestRequestHandler()
# this string base64-encodes to '12345678'
handler.set_secure_cookie("foo", binascii.a2b_hex(b"d76df8e7aefc"), version=1)
handler.set_signed_cookie("foo", binascii.a2b_hex(b"d76df8e7aefc"), version=1)
cookie = handler._cookies["foo"]
match = re.match(rb"12345678\|([0-9]+)\|([0-9a-f]+)", cookie)
assert match is not None
Expand Down Expand Up @@ -160,14 +160,14 @@ def test_cookie_tampering_future_timestamp(self):
)
# it gets rejected
with ExpectLog(gen_log, "Cookie timestamp in future"):
self.assertTrue(handler.get_secure_cookie("foo", min_version=1) is None)
self.assertTrue(handler.get_signed_cookie("foo", min_version=1) is None)

def test_arbitrary_bytes(self):
# Secure cookies accept arbitrary data (which is base64 encoded).
# Note that normal cookies accept only a subset of ascii.
handler = CookieTestRequestHandler()
handler.set_secure_cookie("foo", b"\xe9", version=1)
self.assertEqual(handler.get_secure_cookie("foo", min_version=1), b"\xe9")
handler.set_signed_cookie("foo", b"\xe9", version=1)
self.assertEqual(handler.get_signed_cookie("foo", min_version=1), b"\xe9")


# See SignedValueTest below for more.
Expand All @@ -176,46 +176,46 @@ class SecureCookieV2Test(unittest.TestCase):

def test_round_trip(self):
handler = CookieTestRequestHandler()
handler.set_secure_cookie("foo", b"bar", version=2)
self.assertEqual(handler.get_secure_cookie("foo", min_version=2), b"bar")
handler.set_signed_cookie("foo", b"bar", version=2)
self.assertEqual(handler.get_signed_cookie("foo", min_version=2), b"bar")

def test_key_version_roundtrip(self):
handler = CookieTestRequestHandler(
cookie_secret=self.KEY_VERSIONS, key_version=0
)
handler.set_secure_cookie("foo", b"bar")
self.assertEqual(handler.get_secure_cookie("foo"), b"bar")
handler.set_signed_cookie("foo", b"bar")
self.assertEqual(handler.get_signed_cookie("foo"), b"bar")

def test_key_version_roundtrip_differing_version(self):
handler = CookieTestRequestHandler(
cookie_secret=self.KEY_VERSIONS, key_version=1
)
handler.set_secure_cookie("foo", b"bar")
self.assertEqual(handler.get_secure_cookie("foo"), b"bar")
handler.set_signed_cookie("foo", b"bar")
self.assertEqual(handler.get_signed_cookie("foo"), b"bar")

def test_key_version_increment_version(self):
handler = CookieTestRequestHandler(
cookie_secret=self.KEY_VERSIONS, key_version=0
)
handler.set_secure_cookie("foo", b"bar")
handler.set_signed_cookie("foo", b"bar")
new_handler = CookieTestRequestHandler(
cookie_secret=self.KEY_VERSIONS, key_version=1
)
new_handler._cookies = handler._cookies
self.assertEqual(new_handler.get_secure_cookie("foo"), b"bar")
self.assertEqual(new_handler.get_signed_cookie("foo"), b"bar")

def test_key_version_invalidate_version(self):
handler = CookieTestRequestHandler(
cookie_secret=self.KEY_VERSIONS, key_version=0
)
handler.set_secure_cookie("foo", b"bar")
handler.set_signed_cookie("foo", b"bar")
new_key_versions = self.KEY_VERSIONS.copy()
new_key_versions.pop(0)
new_handler = CookieTestRequestHandler(
cookie_secret=new_key_versions, key_version=1
)
new_handler._cookies = handler._cookies
self.assertEqual(new_handler.get_secure_cookie("foo"), None)
self.assertEqual(new_handler.get_signed_cookie("foo"), None)


class FinalReturnTest(WebTestCase):
Expand Down Expand Up @@ -585,7 +585,7 @@ def prepare(self):
raise Exception(
"unexpected values for cookie keys: %r" % self.cookies.keys()
)
self.check_type("get_secure_cookie", self.get_secure_cookie("asdf"), bytes)
self.check_type("get_signed_cookie", self.get_signed_cookie("asdf"), bytes)
self.check_type("get_cookie", self.get_cookie("asdf"), str)

self.check_type("xsrf_token", self.xsrf_token, bytes)
Expand Down

0 comments on commit 0f8e10b

Please sign in to comment.