diff options
Diffstat (limited to 'service')
-rw-r--r-- | service/pixelated/assets/login.html | 1 | ||||
-rw-r--r-- | service/pixelated/resources/login_resource.py | 6 | ||||
-rw-r--r-- | service/pixelated/resources/session.py | 10 | ||||
-rw-r--r-- | service/test/unit/resources/test_login_resource.py | 13 | ||||
-rw-r--r-- | service/test/unit/resources/test_session.py | 25 |
5 files changed, 55 insertions, 0 deletions
diff --git a/service/pixelated/assets/login.html b/service/pixelated/assets/login.html index ff103f03..c2f5e78e 100644 --- a/service/pixelated/assets/login.html +++ b/service/pixelated/assets/login.html @@ -18,6 +18,7 @@ <form class="standard" id="login_form" action="/login" method="post"> + <input t:render="csrftoken" type="hidden" name="csrftoken" id="csrftoken"><t:attr name="value"><t:slot name="csrftoken" /></t:attr></input> <input type="text" name="username" id="email" class="text-field" placeholder="username" tabindex="1" autofocus="" /> <input type="password" name="password" id="password" class="text-field" placeholder="password" diff --git a/service/pixelated/resources/login_resource.py b/service/pixelated/resources/login_resource.py index fec4307e..7d61ddce 100644 --- a/service/pixelated/resources/login_resource.py +++ b/service/pixelated/resources/login_resource.py @@ -108,6 +108,11 @@ class LoginWebSite(Element): return tag('') @renderer + def csrftoken(self, request, tag): + tag.fillSlots(csrftoken=IPixelatedSession(request.getSession()).get_csrf_token()) + return tag + + @renderer def disclaimer(self, request, tag): return DisclaimerElement(self.disclaimer_banner_file).render(request) @@ -140,6 +145,7 @@ class LoginResource(BaseResource): return NoResource() def render_GET(self, request): + request.getSession() request.setResponseCode(OK) return self._render_template(request) diff --git a/service/pixelated/resources/session.py b/service/pixelated/resources/session.py index 9ade8d29..0e46ad8f 100644 --- a/service/pixelated/resources/session.py +++ b/service/pixelated/resources/session.py @@ -13,11 +13,15 @@ # # You should have received a copy of the GNU Affero General Public License # along with Pixelated. If not, see <http://www.gnu.org/licenses/>. +import hashlib +import os from zope.interface import Interface, Attribute, implements from twisted.python.components import registerAdapter from twisted.web.server import Session +CSRF_TOKEN_LENGTH = 32 + class IPixelatedSession(Interface): user_uuid = Attribute('The uuid of the currently logged in user') @@ -28,6 +32,7 @@ class PixelatedSession(object): def __init__(self, session): self.user_uuid = None + self._csrf_token = None def is_logged_in(self): return self.user_uuid is not None @@ -35,5 +40,10 @@ class PixelatedSession(object): def expire(self): self.user_uuid = None + def get_csrf_token(self): + if self._csrf_token is None: + self._csrf_token = hashlib.sha256(os.urandom(CSRF_TOKEN_LENGTH)).hexdigest() + return self._csrf_token + registerAdapter(PixelatedSession, Session, IPixelatedSession) diff --git a/service/test/unit/resources/test_login_resource.py b/service/test/unit/resources/test_login_resource.py index d3d7ba64..696b0c46 100644 --- a/service/test/unit/resources/test_login_resource.py +++ b/service/test/unit/resources/test_login_resource.py @@ -157,6 +157,19 @@ class TestLoginResource(unittest.TestCase): d.addCallback(assert_default_invalid_banner_disclaimer_rendered) return d + def test_form_should_contain_csrftoken_input(self): + request = DummyRequest(['']) + + d = self.web.get(request) + + def assert_form_has_csrftoken_input(_): + input_username = 'name="csrftoken"' + written_response = ''.join(request.written) + self.assertIn(input_username, written_response) + + d.addCallback(assert_form_has_csrftoken_input) + return d + class TestLoginPOST(unittest.TestCase): def setUp(self): diff --git a/service/test/unit/resources/test_session.py b/service/test/unit/resources/test_session.py new file mode 100644 index 00000000..fe47483d --- /dev/null +++ b/service/test/unit/resources/test_session.py @@ -0,0 +1,25 @@ +from twisted.trial import unittest +from mockito import mock +from pixelated.resources.session import CSRF_TOKEN_LENGTH, PixelatedSession + + +class TestPixelatedSession(unittest.TestCase): + + def setUp(self): + self.pixelated_session = PixelatedSession(mock()) + + def test_csrf_token_should_be_configured_length(self): + self.assertEqual(len(self.pixelated_session.get_csrf_token()), 2 * CSRF_TOKEN_LENGTH) + + def test_csrf_token_should_be_hexdigested(self): + self.assertTrue(all(c in '0123456789abcdef' for c in self.pixelated_session.get_csrf_token())) + + def test_csrf_token_should_always_be_the_same_for_one_session(self): + first_csrf_token = self.pixelated_session.get_csrf_token() + second_csrf_token = self.pixelated_session.get_csrf_token() + self.assertEqual(first_csrf_token, second_csrf_token) + + def test_csrf_token_should_be_different_for_different_session(self): + first_csrf_token = self.pixelated_session.get_csrf_token() + second_csrf_token = PixelatedSession(mock()).get_csrf_token() + self.assertNotEqual(first_csrf_token, second_csrf_token) |