mirror of
https://github.com/spiral-project/ihatemoney.git
synced 2025-05-05 20:51:49 +02:00
Change token path authentication to /PROJECT/token
This commit is contained in:
parent
bbe00ebb57
commit
044638d268
8 changed files with 58 additions and 34 deletions
|
@ -19,6 +19,7 @@ from ihatemoney.models import db
|
||||||
from ihatemoney.utils import (
|
from ihatemoney.utils import (
|
||||||
IhmJSONEncoder,
|
IhmJSONEncoder,
|
||||||
PrefixedWSGI,
|
PrefixedWSGI,
|
||||||
|
RegexConverter,
|
||||||
em_surround,
|
em_surround,
|
||||||
locale_from_iso,
|
locale_from_iso,
|
||||||
localize_list,
|
localize_list,
|
||||||
|
@ -126,6 +127,8 @@ def create_app(
|
||||||
instance_relative_config=instance_relative_config,
|
instance_relative_config=instance_relative_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
app.url_map.converters["regex"] = RegexConverter
|
||||||
|
|
||||||
# If a configuration object is passed, use it. Otherwise try to find one.
|
# If a configuration object is passed, use it. Otherwise try to find one.
|
||||||
load_configuration(app, configuration)
|
load_configuration(app, configuration)
|
||||||
app.wsgi_app = PrefixedWSGI(app)
|
app.wsgi_app = PrefixedWSGI(app)
|
||||||
|
|
|
@ -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.
|
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(".invitation", _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) }}
|
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.
|
If your cookie gets deleted or if you log out, you will need to log back in using the first link.
|
||||||
|
|
|
@ -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.
|
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(".invitation", _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) }}
|
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.
|
Si vous êtes déconnecté volontairement ou non, vous devrez utiliser à nouveau le premier lien.
|
||||||
|
|
|
@ -21,8 +21,8 @@
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ _("You can directly share the following link via your prefered medium") }}</br>
|
{{ _("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()) }}">
|
<a href="{{ url_for(".invitation", _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()) }}
|
{{ url_for(".invitation", _external=True, project_id=g.project.id, token=g.project.generate_token()) }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -213,9 +213,7 @@ class APITestCase(IhatemoneyTestCase):
|
||||||
"/api/projects/raclette/token", headers=self.get_auth("raclette")
|
"/api/projects/raclette/token", headers=self.get_auth("raclette")
|
||||||
)
|
)
|
||||||
decoded_resp = json.loads(resp.data.decode("utf-8"))
|
decoded_resp = json.loads(resp.data.decode("utf-8"))
|
||||||
resp = self.client.get(
|
resp = self.client.get(f"/raclette/{decoded_resp['token']}")
|
||||||
f"/authenticate?token={decoded_resp['token']}&project_id=raclette"
|
|
||||||
)
|
|
||||||
# Test that we are redirected.
|
# Test that we are redirected.
|
||||||
self.assertEqual(302, resp.status_code)
|
self.assertEqual(302, resp.status_code)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import json
|
||||||
import re
|
import re
|
||||||
from time import sleep
|
from time import sleep
|
||||||
import unittest
|
import unittest
|
||||||
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
|
from urllib.parse import urlparse, urlunparse
|
||||||
|
|
||||||
from flask import session
|
from flask import session
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -91,16 +91,20 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
self.client.get("/exit")
|
self.client.get("/exit")
|
||||||
# Use another project_id
|
# Use another project_id
|
||||||
parsed_url = urlparse(url)
|
parsed_url = urlparse(url)
|
||||||
query = parse_qs(parsed_url.query)
|
|
||||||
query["project_id"] = "invalid"
|
|
||||||
resp = self.client.get(
|
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")
|
resp = self.client.get("/authenticate")
|
||||||
self.assertIn("You either provided a bad token", resp.data.decode("utf-8"))
|
self.assertIn("You either provided a bad token", resp.data.decode("utf-8"))
|
||||||
resp = self.client.get("/authenticate?token=token")
|
# A token MUST have a point between payload and signature
|
||||||
|
resp = self.client.get("/raclette/token.invalid", follow_redirects=True)
|
||||||
self.assertIn("You either provided a bad token", resp.data.decode("utf-8"))
|
self.assertIn("You either provided a bad token", resp.data.decode("utf-8"))
|
||||||
|
|
||||||
def test_invite_code_invalidation(self):
|
def test_invite_code_invalidation(self):
|
||||||
|
|
|
@ -16,7 +16,7 @@ from flask import current_app, escape, redirect, render_template
|
||||||
from flask_babel import get_locale, lazy_gettext as _
|
from flask_babel import get_locale, lazy_gettext as _
|
||||||
import jinja2
|
import jinja2
|
||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
from werkzeug.routing import HTTPException, RoutingException
|
from werkzeug.routing import BaseConverter, HTTPException, RoutingException
|
||||||
|
|
||||||
|
|
||||||
def slugify(value):
|
def slugify(value):
|
||||||
|
@ -416,3 +416,10 @@ def format_form_errors(form, prefix):
|
||||||
errors = f"<ul><li>{error_list}</li></ul>"
|
errors = f"<ul><li>{error_list}</li></ul>"
|
||||||
# I18N: Form error with a list of errors
|
# I18N: Form error with a list of errors
|
||||||
return Markup(_("{prefix}:<br />{errors}").format(prefix=prefix, errors=errors))
|
return Markup(_("{prefix}:<br />{errors}").format(prefix=prefix, errors=errors))
|
||||||
|
|
||||||
|
|
||||||
|
# Taken from https://stackoverflow.com/a/5872904
|
||||||
|
class RegexConverter(BaseConverter):
|
||||||
|
def __init__(self, url_map, *items):
|
||||||
|
super(RegexConverter, self).__init__(url_map)
|
||||||
|
self.regex = items[0]
|
||||||
|
|
|
@ -143,7 +143,8 @@ def pull_project(endpoint, values):
|
||||||
raise Redirect303(url_for(".create_project", project_id=project_id))
|
raise Redirect303(url_for(".create_project", project_id=project_id))
|
||||||
|
|
||||||
is_admin = session.get("is_admin")
|
is_admin = session.get("is_admin")
|
||||||
if session.get(project.id) or is_admin:
|
is_invitation = endpoint == "main.invitation"
|
||||||
|
if session.get(project.id) or is_admin or is_invitation:
|
||||||
# add project into kwargs and call the original function
|
# add project into kwargs and call the original function
|
||||||
g.project = project
|
g.project = project
|
||||||
else:
|
else:
|
||||||
|
@ -195,6 +196,32 @@ def admin():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# To avoid matching other endpoint with a malformed token,
|
||||||
|
# ensure that it has a point in the middle, since it's the
|
||||||
|
# default separator between payload and signature.
|
||||||
|
@main.route("/<project_id>/<regex('.+\\..+'):token>", methods=["GET"])
|
||||||
|
def invitation(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:
|
||||||
|
# User doesn't provide project identifier or a valid token
|
||||||
|
# redirect to authenticate form
|
||||||
|
return redirect(url_for(".authenticate", project_id=project_id, bad_token=1))
|
||||||
|
|
||||||
|
# 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"])
|
@main.route("/authenticate", methods=["GET", "POST"])
|
||||||
def authenticate(project_id=None):
|
def authenticate(project_id=None):
|
||||||
"""Authentication form"""
|
"""Authentication form"""
|
||||||
|
@ -203,19 +230,8 @@ def authenticate(project_id=None):
|
||||||
if not form.id.data and request.args.get("project_id"):
|
if not form.id.data and request.args.get("project_id"):
|
||||||
form.id.data = request.args["project_id"]
|
form.id.data = request.args["project_id"]
|
||||||
project_id = form.id.data
|
project_id = form.id.data
|
||||||
# Try to get project_id from token first
|
|
||||||
token = request.args.get("token")
|
if project_id is None or request.args.get("bad_token") is not None:
|
||||||
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
|
# User doesn't provide project identifier or a valid token
|
||||||
# return to authenticate form
|
# return to authenticate form
|
||||||
msg = _("You either provided a bad token or no project identifier.")
|
msg = _("You either provided a bad token or no project identifier.")
|
||||||
|
@ -235,13 +251,9 @@ def authenticate(project_id=None):
|
||||||
setattr(g, "project", project)
|
setattr(g, "project", project)
|
||||||
return redirect(url_for(".list_bills"))
|
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()
|
is_post_auth = request.method == "POST" and form.validate()
|
||||||
if (
|
if is_post_auth and check_password_hash(project.password, form.password.data):
|
||||||
is_post_auth
|
|
||||||
and check_password_hash(project.password, form.password.data)
|
|
||||||
or token_auth
|
|
||||||
):
|
|
||||||
# maintain a list of visited projects
|
# maintain a list of visited projects
|
||||||
if "projects" not in session:
|
if "projects" not in session:
|
||||||
session["projects"] = []
|
session["projects"] = []
|
||||||
|
|
Loading…
Reference in a new issue