diff --git a/ihatemoney/api/common.py b/ihatemoney/api/common.py index 70a706c4..c33ee70c 100644 --- a/ihatemoney/api/common.py +++ b/ihatemoney/api/common.py @@ -35,7 +35,7 @@ def need_auth(f): auth_token = auth_header.split(" ")[1] except IndexError: abort(401) - project_id = Project.verify_token(auth_token, token_type="auth") + project_id = Project.verify_token(auth_token, token_type="auth", project_id=project_id) if auth_token and project_id: project = Project.query.get(project_id) if project: diff --git a/ihatemoney/models.py b/ihatemoney/models.py index aa64e4ac..ddf9b714 100644 --- a/ihatemoney/models.py +++ b/ihatemoney/models.py @@ -353,20 +353,21 @@ class Project(db.Model): token = serializer.dumps({"project_id": self.id}) else: serializer = URLSafeSerializer( - current_app.config["SECRET_KEY"], salt=token_type + current_app.config["SECRET_KEY"] + self.password, salt=token_type ) - token = serializer.dumps({"project_id": self.id, "password": self.password}) + token = serializer.dumps({"project_id": self.id}) return token @staticmethod - def verify_token(token, token_type="auth", max_age=3600): + def verify_token(token, token_type="auth", project_id=None, max_age=3600): """Return the project id associated to the provided token, None if the provided token is expired or not valid. :param token: Serialized TimedJsonWebToken :param token_type: Either "auth" for authentication (invalidated when project code changed), or "reset" for password reset (invalidated after expiration) + :param project_id: Project ID. Used for token_type "auth" to use the password as serializer secret key. :param max_age: Token expiration time (in seconds). Only used with token_type "reset" """ loads_kwargs = {} @@ -376,8 +377,10 @@ class Project(db.Model): ) loads_kwargs["max_age"] = max_age else: + project = Project.query.get(project_id) + password = project.password if project is not None else '' serializer = URLSafeSerializer( - current_app.config["SECRET_KEY"], salt=token_type + current_app.config["SECRET_KEY"] + password, salt=token_type ) try: data = serializer.loads(token, **loads_kwargs) @@ -386,13 +389,8 @@ class Project(db.Model): except BadSignature: return None - password = data.get("password", None) - project_id = data["project_id"] - if password is not None: - project = Project.query.get(project_id) - if project is None or project.password != password: - return None - return project_id + data_project = data.get("project_id") + return data_project if project_id is None or data_project == project_id else None def __str__(self): return self.name diff --git a/ihatemoney/templates/invitation_mail.en.j2 b/ihatemoney/templates/invitation_mail.en.j2 index 79fcc427..2b3157b1 100644 --- a/ihatemoney/templates/invitation_mail.en.j2 +++ b/ihatemoney/templates/invitation_mail.en.j2 @@ -4,7 +4,7 @@ Someone using the email address {{ g.project.contact_email }} invited you to sha It's as simple as saying what did you pay for, for whom, and how much did it cost you, we are caring about the rest. -You can log in using this link: {{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }}. +You can log in using this link: {{ url_for(".authenticate", _external=True, project_id=g.project.id, 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. diff --git a/ihatemoney/templates/invitation_mail.fr.j2 b/ihatemoney/templates/invitation_mail.fr.j2 index e57d7035..d095cfdb 100644 --- a/ihatemoney/templates/invitation_mail.fr.j2 +++ b/ihatemoney/templates/invitation_mail.fr.j2 @@ -4,7 +4,7 @@ Quelqu'un dont l'adresse email est {{ g.project.contact_email }} vous a invité Il suffit de renseigner qui a payé pour quoi, pour qui, combien ça a coûté, et on s’occupe du reste. -Vous pouvez vous connecter grâce à ce lien : {{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }}. +Vous pouvez vous connecter grâce à ce lien : {{ url_for(".authenticate", _external=True, project_id=g.project.id, token=g.project.generate_token()) }}. Une fois connecté, vous pourrez utiliser le lien suivant qui est plus facile à mémoriser : {{ url_for(".list_bills", _external=True) }} Si vous êtes déconnecté volontairement ou non, vous devrez utiliser à nouveau le premier lien. diff --git a/ihatemoney/templates/send_invites.html b/ihatemoney/templates/send_invites.html index 53492c85..8b73b175 100644 --- a/ihatemoney/templates/send_invites.html +++ b/ihatemoney/templates/send_invites.html @@ -21,8 +21,8 @@ {{ _("You can directly share the following link via your prefered medium") }}
- - {{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }} + + {{ url_for(".authenticate", _external=True, project_id=g.project.id, token=g.project.generate_token()) }} diff --git a/ihatemoney/tests/api_test.py b/ihatemoney/tests/api_test.py index 41f5ab2d..0894b15b 100644 --- a/ihatemoney/tests/api_test.py +++ b/ihatemoney/tests/api_test.py @@ -213,7 +213,7 @@ class APITestCase(IhatemoneyTestCase): "/api/projects/raclette/token", headers=self.get_auth("raclette") ) decoded_resp = json.loads(resp.data.decode("utf-8")) - resp = self.client.get("/authenticate?token={}".format(decoded_resp["token"])) + resp = self.client.get(f"/authenticate?token={decoded_resp['token']}&project_id=raclette") # Test that we are redirected. self.assertEqual(302, resp.status_code) diff --git a/ihatemoney/tests/budget_test.py b/ihatemoney/tests/budget_test.py index dc862f02..6c858124 100644 --- a/ihatemoney/tests/budget_test.py +++ b/ihatemoney/tests/budget_test.py @@ -6,6 +6,7 @@ from time import sleep import unittest from flask import session +from markupsafe import Markup import pytest from werkzeug.security import check_password_hash, generate_password_hash @@ -100,7 +101,7 @@ class BudgetTestCase(IhatemoneyTestCase): base_index = response.find("share the following link") start = response.find('href="', base_index) + 6 end = response.find('">', base_index) - link = response[start:end] + link = Markup(response[start:end]).unescape() self.client.get("/exit") response = self.client.get(link) diff --git a/ihatemoney/web.py b/ihatemoney/web.py index b2d72f55..0f75d640 100644 --- a/ihatemoney/web.py +++ b/ihatemoney/web.py @@ -199,15 +199,16 @@ def admin(): def authenticate(project_id=None): """Authentication form""" form = AuthenticationForm() + + if not form.id.data and request.args.get("project_id"): + form.id.data = request.args["project_id"] + project_id = form.id.data # Try to get project_id from token first token = request.args.get("token") if token: - project_id = Project.verify_token(token, token_type="auth") + project_id = Project.verify_token(token, token_type="auth", project_id=project_id) token_auth = True else: - if not form.id.data and request.args.get("project_id"): - form.id.data = request.args["project_id"] - project_id = form.id.data token_auth = False if project_id is None: # User doesn't provide project identifier or a valid token