Skip to content

Commit

Permalink
Change token path authentication to /PROJECT/join/TOKEN (#843)
Browse files Browse the repository at this point in the history
  • Loading branch information
Glandos committed Oct 13, 2021
1 parent cb8f6aa commit 7d92267
Show file tree
Hide file tree
Showing 6 changed files with 44 additions and 43 deletions.
2 changes: 1 addition & 1 deletion ihatemoney/templates/invitation_mail.en.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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, project_id=g.project.id, token=g.project.generate_token()) }}.
You can log in using this link: {{ url_for(".join_project", _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.
Expand Down
2 changes: 1 addition & 1 deletion ihatemoney/templates/invitation_mail.fr.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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, project_id=g.project.id, token=g.project.generate_token()) }}.
Vous pouvez vous connecter grâce à ce lien : {{ url_for(".join_project", _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.
Expand Down
4 changes: 2 additions & 2 deletions ihatemoney/templates/send_invites.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ <h3>{{ _('Share the Link') }}</h3>
</td>
<td>
{{ _("You can directly share the following link via your prefered medium") }}</br>
<a href="{{ url_for(".authenticate", _external=True, project_id=g.project.id, token=g.project.generate_token()) }}">
{{ url_for(".authenticate", _external=True, project_id=g.project.id, token=g.project.generate_token()) }}
<a href="{{ url_for(".join_project", _external=True, project_id=g.project.id, token=g.project.generate_token()) }}">
{{ url_for(".join_project", _external=True, project_id=g.project.id, token=g.project.generate_token()) }}
</a>
</td>
</tr>
Expand Down
4 changes: 1 addition & 3 deletions ihatemoney/tests/api_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,7 @@ def test_token_login(self):
"/api/projects/raclette/token", headers=self.get_auth("raclette")
)
decoded_resp = json.loads(resp.data.decode("utf-8"))
resp = self.client.get(
f"/authenticate?token={decoded_resp['token']}&project_id=raclette"
)
resp = self.client.get(f"/raclette/join/{decoded_resp['token']}")
# Test that we are redirected.
self.assertEqual(302, resp.status_code)

Expand Down
22 changes: 12 additions & 10 deletions ihatemoney/tests/budget_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import re
from time import sleep
import unittest
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
from urllib.parse import urlparse, urlunparse

from flask import session
import pytest
Expand Down Expand Up @@ -91,17 +91,19 @@ def test_invite(self):
self.client.get("/exit")
# Use another project_id
parsed_url = urlparse(url)
query = parse_qs(parsed_url.query)
query["project_id"] = "invalid"
resp = self.client.get(
urlunparse(parsed_url._replace(query=urlencode(query, doseq=True)))
urlunparse(
parsed_url._replace(
path=parsed_url.path.replace("raclette/", "invalid_project/")
)
),
follow_redirects=True,
)
assert "You either provided a bad token" in resp.data.decode("utf-8")
assert "Create a new project" in resp.data.decode("utf-8")

resp = self.client.get("/authenticate")
self.assertIn("You either provided a bad token", resp.data.decode("utf-8"))
resp = self.client.get("/authenticate?token=token")
self.assertIn("You either provided a bad token", resp.data.decode("utf-8"))
# A token MUST have a point between payload and signature
resp = self.client.get("/raclette/join/token.invalid", follow_redirects=True)
self.assertIn("Provided token is invalid", resp.data.decode("utf-8"))

def test_invite_code_invalidation(self):
"""Test that invitation link expire after code change"""
Expand Down Expand Up @@ -133,7 +135,7 @@ def test_invite_code_invalidation(self):
self.client.get("/exit")
response = self.client.get(link, follow_redirects=True)
# Link is invalid
self.assertIn("You either provided a bad token", response.data.decode("utf-8"))
self.assertIn("Provided token is invalid", response.data.decode("utf-8"))

def test_password_reminder(self):
# test that it is possible to have an email containing the password of a
Expand Down
53 changes: 27 additions & 26 deletions ihatemoney/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ def pull_project(endpoint, values):
raise Redirect303(url_for(".create_project", project_id=project_id))

is_admin = session.get("is_admin")
if session.get(project.id) or is_admin:
is_invitation = endpoint == "main.join_project"
if session.get(project.id) or is_admin or is_invitation:
# add project into kwargs and call the original function
g.project = project
else:
Expand Down Expand Up @@ -195,6 +196,28 @@ def admin():
)


@main.route("/<project_id>/join/<string:token>", methods=["GET"])
def join_project(token):
project_id = g.project.id
verified_project_id = Project.verify_token(
token, token_type="auth", project_id=project_id
)
if verified_project_id != project_id:
flash(_("Provided token is invalid"), "danger")
return redirect("/")

# maintain a list of visited projects
if "projects" not in session:
session["projects"] = []
# add the project on the top of the list
session["projects"].insert(0, (project_id, g.project.name))
session[project_id] = True
# Set session to permanent to make language choice persist
session.permanent = True
session.update()
return redirect(url_for(".list_bills"))


@main.route("/authenticate", methods=["GET", "POST"])
def authenticate(project_id=None):
"""Authentication form"""
Expand All @@ -203,26 +226,8 @@ def authenticate(project_id=None):
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:
verified_project_id = Project.verify_token(
token, token_type="auth", project_id=project_id
)
if verified_project_id == project_id:
token_auth = True
else:
project_id = None
else:
token_auth = False
if project_id is None:
# User doesn't provide project identifier or a valid token
# return to authenticate form
msg = _("You either provided a bad token or no project identifier.")
form["id"].errors = [msg]
return render_template("authenticate.html", form=form)

project = Project.query.get(project_id)
project = Project.query.get(project_id) if project_id is not None else None
if not project:
# If the user try to connect to an unexisting project, we will
# propose him a link to the creation form.
Expand All @@ -235,13 +240,9 @@ def authenticate(project_id=None):
setattr(g, "project", project)
return redirect(url_for(".list_bills"))

# else do form authentication or token authentication
# else do form authentication authentication
is_post_auth = request.method == "POST" and form.validate()
if (
is_post_auth
and check_password_hash(project.password, form.password.data)
or token_auth
):
if is_post_auth and check_password_hash(project.password, form.password.data):
# maintain a list of visited projects
if "projects" not in session:
session["projects"] = []
Expand Down

0 comments on commit 7d92267

Please sign in to comment.