summaryrefslogtreecommitdiff
path: root/service
diff options
context:
space:
mode:
authorRoald de Vries <rdevries@thoughtworks.com>2016-11-30 16:11:27 +0100
committerRoald de Vries <rdevries@thoughtworks.com>2016-11-30 16:11:27 +0100
commit13378255c02b97184132881599ed47826963f54a (patch)
tree01a47f844f581a12dae9d022be19d4010433633e /service
parenta493da72d53fe90d679d7fa1980dd185415d9be3 (diff)
add csrf token to login form
Diffstat (limited to 'service')
-rw-r--r--service/pixelated/assets/login.html1
-rw-r--r--service/pixelated/resources/login_resource.py6
-rw-r--r--service/pixelated/resources/session.py10
-rw-r--r--service/test/unit/resources/test_login_resource.py13
-rw-r--r--service/test/unit/resources/test_session.py25
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)