From e52fbbd2395fb5711c98f684dcb8389a52553772 Mon Sep 17 00:00:00 2001 From: 0livd Date: Fri, 27 Oct 2017 17:57:11 +0200 Subject: [PATCH] Use token based auth in invitation e-mails Invitation e-mails no longer contain the clear text project password --- CHANGELOG.rst | 1 + ihatemoney/models.py | 24 ++++++++++---- ihatemoney/templates/authenticate.html | 5 +-- ihatemoney/templates/invitation_mail.en | 4 ++- ihatemoney/templates/invitation_mail.fr | 4 ++- ihatemoney/tests/tests.py | 23 ++++++++++++++ .../translations/fr/LC_MESSAGES/messages.mo | Bin 9559 -> 9694 bytes .../translations/fr/LC_MESSAGES/messages.po | 7 ++-- ihatemoney/web.py | 30 ++++++++++++------ 9 files changed, 75 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2f3a0375..127eabc3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,6 +20,7 @@ Changed - Simpler and safer authentication logic (#270) - Use token based auth to reset passwords (#269) - Better install doc (#275) +- Use token based auth in invitation e-mails Added ===== diff --git a/ihatemoney/models.py b/ihatemoney/models.py index c801b745..9e11054d 100644 --- a/ihatemoney/models.py +++ b/ihatemoney/models.py @@ -5,8 +5,8 @@ from flask_sqlalchemy import SQLAlchemy, BaseQuery from flask import g, current_app from sqlalchemy import orm -from itsdangerous import (TimedJSONWebSignatureSerializer - as Serializer, BadSignature, SignatureExpired) +from itsdangerous import (TimedJSONWebSignatureSerializer, URLSafeSerializer, + BadSignature, SignatureExpired) db = SQLAlchemy() @@ -201,22 +201,32 @@ class Project(db.Model): db.session.delete(self) db.session.commit() - def generate_token(self, expiration): + def generate_token(self, expiration=0): """Generate a timed and serialized JsonWebToken :param expiration: Token expiration time (in seconds) """ - serializer = Serializer(current_app.config['SECRET_KEY'], expiration) - return serializer.dumps({'project_id': self.id}).decode('utf-8') + if expiration: + serializer = TimedJSONWebSignatureSerializer( + current_app.config['SECRET_KEY'], + expiration) + token = serializer.dumps({'project_id': self.id}).decode('utf-8') + else: + serializer = URLSafeSerializer(current_app.config['SECRET_KEY']) + token = serializer.dumps({'project_id': self.id}) + return token @staticmethod - def verify_token(token): + def verify_token(token, token_type="timed_token"): """Return the project id associated to the provided token, None if the provided token is expired or not valid. :param token: Serialized TimedJsonWebToken """ - serializer = Serializer(current_app.config['SECRET_KEY']) + if token_type == "timed_token": + serializer = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY']) + else: + serializer = URLSafeSerializer(current_app.config['SECRET_KEY']) try: data = serializer.loads(token) except SignatureExpired: diff --git a/ihatemoney/templates/authenticate.html b/ihatemoney/templates/authenticate.html index 98914d09..4e8eb779 100644 --- a/ihatemoney/templates/authenticate.html +++ b/ihatemoney/templates/authenticate.html @@ -3,8 +3,9 @@

Authentication

{% if create_project %} -

{{ _("The project you are trying to access do not exist, do you want -to") }} {{ _("create it") }}{{ _("?") }} +

{{ _("The project you are trying to access do not exist, do you want to") }} + + {{ _("create it") }}{{ _("?") }}

{% endif %}
diff --git a/ihatemoney/templates/invitation_mail.en b/ihatemoney/templates/invitation_mail.en index 03f51412..eeaafdb9 100644 --- a/ihatemoney/templates/invitation_mail.en +++ b/ihatemoney/templates/invitation_mail.en @@ -4,7 +4,9 @@ Someone using the email address {{ g.project.contact_email }} invited you to sha It's as simple as saying what did you paid for, for who, and how much did it cost you, we are caring about the rest. -You can access it here: {{ config['SITE_URL'] }}{{ url_for(".list_bills") }} and the private code is "{{ g.project.password }}". +You can log in using this link: {{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }}. +Once logged in you can use the following link which is easier to remember: {{ url_for(".list_bills", _external=True) }} +If your cookie gets deleted or if you log out, you will need to log back in using the first link. Enjoy, Some weird guys (with beards) diff --git a/ihatemoney/templates/invitation_mail.fr b/ihatemoney/templates/invitation_mail.fr index 53698ddc..a95f9e9e 100644 --- a/ihatemoney/templates/invitation_mail.fr +++ b/ihatemoney/templates/invitation_mail.fr @@ -4,6 +4,8 @@ Quelqu'un avec l'addresse email "{{ g.project.contact_email }}" vous à invité C'est aussi simple que de dire qui à payé pour quoi, pour qui, et combien celà à coûté, on s'occuppe du reste. -Vous pouvez accéder à la page ici: {{ config['SITE_URL'] }}{{ url_for(".list_bills") }} et le code est "{{ g.project.password }}". +Vous pouvez vous authentifier avec le lien suivant: {{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }}. +Une fois authentifié, vous pouvez utiliser le lien suivant qui est plus facile à mémoriser: {{ url_for(".list_bills", _external=True) }} +Si votre cookie est supprimé ou si vous vous déconnectez, voous devrez vous réauthentifier en utilisant le premier lien. Have fun, diff --git a/ihatemoney/tests/tests.py b/ihatemoney/tests/tests.py index f9187461..fd00935e 100644 --- a/ihatemoney/tests/tests.py +++ b/ihatemoney/tests/tests.py @@ -152,6 +152,29 @@ class BudgetTestCase(IhatemoneyTestCase): # only one message is sent to multiple persons self.assertEqual(len(outbox), 0) + def test_invite(self): + """Test that invitation e-mails are sent properly + """ + self.login("raclette") + self.post_project("raclette") + with self.app.mail.record_messages() as outbox: + self.client.post("/raclette/invite", + data={"emails": 'toto@notmyidea.org'}) + self.assertEqual(len(outbox), 1) + url_start = outbox[0].body.find('You can log in using this link: ') + 32 + url_end = outbox[0].body.find('.\n', url_start) + url = outbox[0].body[url_start:url_end] + self.client.get("/exit") + # Test that we got a valid token + resp = self.client.get(url, follow_redirects=True) + self.assertIn('You probably want to Em#tbZa#6;Q!@HzAb^;?czr%jc zQOk@rTA9^iVOz`S2V1aKj&xRkFxy;n(OUjdmRs&{<-*nTb$qvOclY>wKi|*y^LfAD zpU>Ud;LKg_OkUcM@jA$-h)-9VuKs^bWSHf1If;chjWzf?mZNWx*#d09I&8vhJb(-F zWlY7_@LD{ASvWecYvVL3={SjtaTaq>qcuS;reh&0kYZGzwRjEQi%YQ?^RNq*gNwvy zPou`afSUIhDuB^59V*ZuYFrEIxiBi=$8j0< zp~ek*9zj*$LsWv_p(=3(T|Rc6FO~WdD&s4t{!G%)z(Ujn0n`>Wp#ppe6-Xy)d>nN~ zT-25fqcZ&fOYjqX2hXAc*_&l4;NL=z5q9z{L)J{I6dSb^W*b(lacw3xD}q<(bp zHdKEr{)&5X6LyeAFOFaX7EvC})0Rv9%W3SP!;f#FGCheZ>3OWhi>OMJP##sF2ASI$ zQI&fDSK}ktg-@Zz{fVkf8W&0^7kyZOs=#{JYt*9-*A~yaQG45rw_pq2im#%6W@k_t zWKm}w!g8dl)`+U)HdN(ypcd-KRk$Cu&g-ag?g$M{~nd;Ib?17 z1vSB6s0>p$dF+a1qZaU?`q!XN|2nV#K|I7CMjH~4YqR`jltCWpRALQk3wB@%?!!iO zk!sr4s6c-3{23L{Z>TN0iYqaT^y@Kz8Xrd1v~E;Ip2ZB-w?P^=&@qJ9;}q(F1oF0j z_|n8nIa#WZA64oAR^ZL3N#)IxutPH!4tx-LcCufz3tCl25M>iJa4l{}27iuh28RG|Xefcm*^ z@lk(G5T?VyqsScgC9os!1qvlJ3Tg< zabe-uD>;)X>q3#9a47C{$76kwj!=iw>a?|XI6bjlp{Ns!JJFbKJr-*3akw4riF8In z@v5<{`7Nmns>eo`4`v>|Yr$C;pN?547^ Gl>Y$GzVTcD delta 2268 zcmYM!e`u9e9LMoQxV3jQ7p6Jct{A+ zM9i`u6T})p=_=?}rp_QtqwFtni!fKW8MqM2!Jx9T_xs)l8ryxH=iKKx=X*ZqJR4c_ zU0XC#I(c8r0jp!P>kQzfb zdVeoEZXa5};p}x3!Z8{%=s1H;bOC*E6dmv~QcJiNo1{u7F2gFUK)-Lna$Jei@uAqq zaX#12Vg>f0&wqmT%pV46Sn);VH;nPYTwwwiVVIVS^d_{>7WBS$^tlJof;(^~K8D`6 zEp|7Wz(I6@C(tBLV#HrK$A?j8&=rrN{eSb}jY*c{02SyKG@}KsK?_-r-v2OqM!L`~ z*@3R~0M_6;*pFYKg>)8$C_CU&Y={HC5W5F`a6jIFZ(}_k$5}XvPIMKGbS5XR8keH| z58x&2#1*)XRXl^Q5UCGc?j6Y{U^XiBjS*fm&qj(2ORx z3U9>wa5HwJ_hryzCeQ`t(OHQ}G=YUt+*pDhqFZBI(Y;-b%kWfIUL{I(YxWAZ_`7-|~W}^ii=2c+%LksWGI{@xH}I97Yo@B(BIx=h5)S6q>+lbZ_rR zx1d^jW=zMMHLN?ANe~JlD&|yR`qAPtB zov0r@EXU*P@8j>UU^D%G`8?c?K7S0ehY?L=2wli8XhGxXYx)=Zd{rg+SJT))&h&+? z$k^dEH2QSxS^S3UE4UH|Il8@=s1D%~jL?OQ;u8E9>#(6Fm&`i!t?9&e+=3=>GNR!r zK8IF(361IxT!>TH;2umN>4rnd8x>BYhwD62OUR&Gk*LkBv;ebPflgG3+5ZQ)g6k%1 z#b_%HD?frxcovQFDq2`Rd2#N-9Q3tm!Uk+b57lP${w?wKcC?UPXymV;h3rSS;=Q>4 zQ)G)H4>Kcf>*pc5t+_VY