diff --git a/ihatemoney/api/common.py b/ihatemoney/api/common.py index ede76e46..fa097dec 100644 --- a/ihatemoney/api/common.py +++ b/ihatemoney/api/common.py @@ -35,7 +35,9 @@ def need_auth(f): auth_token = auth_header.split(" ")[1] except IndexError: abort(401) - project_id = Project.verify_token(auth_token, token_type="non_timed_token") + 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 68d1fd84..b3d1cba2 100644 --- a/ihatemoney/models.py +++ b/ihatemoney/models.py @@ -7,8 +7,8 @@ from flask_sqlalchemy import BaseQuery, SQLAlchemy from itsdangerous import ( BadSignature, SignatureExpired, - TimedJSONWebSignatureSerializer, URLSafeSerializer, + URLSafeTimedSerializer, ) import sqlalchemy from sqlalchemy import orm @@ -339,41 +339,61 @@ class Project(db.Model): db.session.delete(self) db.session.commit() - def generate_token(self, expiration=0): + def generate_token(self, token_type="auth"): """Generate a timed and serialized JsonWebToken - :param expiration: Token expiration time (in seconds) + :param token_type: Either "auth" for authentication (invalidated when project code changed), + or "reset" for password reset (invalidated after expiration) """ - if expiration: - serializer = TimedJSONWebSignatureSerializer( - current_app.config["SECRET_KEY"], expiration + + if token_type == "reset": + serializer = URLSafeTimedSerializer( + current_app.config["SECRET_KEY"], salt=token_type ) - token = serializer.dumps({"project_id": self.id}).decode("utf-8") + token = serializer.dumps([self.id]) else: - serializer = URLSafeSerializer(current_app.config["SECRET_KEY"]) - token = serializer.dumps({"project_id": self.id}) + serializer = URLSafeSerializer( + current_app.config["SECRET_KEY"] + self.password, salt=token_type + ) + token = serializer.dumps([self.id]) + return token @staticmethod - def verify_token(token, token_type="timed_token"): + 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" """ - if token_type == "timed_token": - serializer = TimedJSONWebSignatureSerializer( - current_app.config["SECRET_KEY"] + loads_kwargs = {} + if token_type == "reset": + serializer = URLSafeTimedSerializer( + current_app.config["SECRET_KEY"], salt=token_type ) + loads_kwargs["max_age"] = max_age else: - serializer = URLSafeSerializer(current_app.config["SECRET_KEY"]) + project = Project.query.get(project_id) if project_id is not None else None + password = project.password if project is not None else "" + serializer = URLSafeSerializer( + current_app.config["SECRET_KEY"] + password, salt=token_type + ) try: - data = serializer.loads(token) + data = serializer.loads(token, **loads_kwargs) except SignatureExpired: return None except BadSignature: return None - return data["project_id"] + + data_project = data[0] if isinstance(data, list) else None + 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/password_reminder.en.j2 b/ihatemoney/templates/password_reminder.en.j2 index c6543546..845ff790 100644 --- a/ihatemoney/templates/password_reminder.en.j2 +++ b/ihatemoney/templates/password_reminder.en.j2 @@ -1,7 +1,7 @@ Hi, You requested to reset the password of the following project: "{{ project.name }}". -You can reset it here: {{ url_for(".reset_password", _external=True, token=project.generate_token(expiration=3600)) }}. +You can reset it here: {{ url_for(".reset_password", _external=True, token=project.generate_token(token_type="reset")) }}. This link is only valid for one hour. Hope this helps, diff --git a/ihatemoney/templates/password_reminder.fr.j2 b/ihatemoney/templates/password_reminder.fr.j2 index 17c52c4d..4603a963 100644 --- a/ihatemoney/templates/password_reminder.fr.j2 +++ b/ihatemoney/templates/password_reminder.fr.j2 @@ -1,7 +1,7 @@ Salut, Vous avez demandé à réinitialiser le mot de passe du projet suivant : "{{ project.name }}". -Vous pouvez le réinitialiser ici : {{ url_for(".reset_password", _external=True, token=project.generate_token(expiration=3600)) }}. +Vous pouvez le réinitialiser ici : {{ url_for(".reset_password", _external=True, token=project.generate_token(token_type="reset")) }}. Ce lien est seulement valide pendant 1 heure. Faites-en bon usage ! 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 @@