mirror of
https://github.com/spiral-project/ihatemoney.git
synced 2025-05-05 20:51:49 +02:00
API BREAKING CHANGE the authenticate token now need project_id= param in URL
This change is introduced to have the ability to invalidate auth token with password change. Token payload is still the same, but the key is the concatenation of SECRET_KEY project password. To have a clean verification, we need to have the project id before loading payload, to build the serializer with the correct key (including the password).
This commit is contained in:
parent
57525fc2b6
commit
d9a4389d42
8 changed files with 22 additions and 22 deletions
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -21,8 +21,8 @@
|
|||
</td>
|
||||
<td>
|
||||
{{ _("You can directly share the following link via your prefered medium") }}</br>
|
||||
<a href="{{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }}">
|
||||
{{ url_for(".authenticate", _external=True, token=g.project.generate_token()) }}
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue