From 176dc8f0613a80c3f15037894dc5ef1563ea30ee Mon Sep 17 00:00:00 2001 From: Martin Varga Date: Fri, 4 Jul 2025 08:57:53 +0200 Subject: [PATCH 1/2] Custom bearer token expiration validation --- server/mergin/app.py | 1 - server/mergin/auth/bearer.py | 15 +++++++-- server/mergin/tests/test_auth.py | 58 +++++++++++++++++++++++++++++++- 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/server/mergin/app.py b/server/mergin/app.py index 8910c6d1..d0fd2f3a 100644 --- a/server/mergin/app.py +++ b/server/mergin/app.py @@ -209,7 +209,6 @@ def load_user_from_header(header_val): # pylint: disable=W0613,W0612 app.app.config["SECRET_KEY"], app.app.config["SECURITY_BEARER_SALT"], header_val, - app.app.config["BEARER_TOKEN_EXPIRATION"], ) user = User.query.filter_by( id=data["user_id"], username=data["username"], email=data["email"] diff --git a/server/mergin/auth/bearer.py b/server/mergin/auth/bearer.py index 1c54a054..adadec77 100644 --- a/server/mergin/auth/bearer.py +++ b/server/mergin/auth/bearer.py @@ -3,17 +3,28 @@ # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial import hashlib +from datetime import datetime, timezone from itsdangerous import URLSafeTimedSerializer +from itsdangerous.exc import SignatureExpired, BadSignature from flask.sessions import TaggedJSONSerializer -def decode_token(secret_key, salt, token, max_age=None): +def decode_token(secret_key, salt, token): serializer = TaggedJSONSerializer() signer_kwargs = {"key_derivation": "hmac", "digest_method": hashlib.sha1} s = URLSafeTimedSerializer( secret_key, salt=salt, serializer=serializer, signer_kwargs=signer_kwargs ) - return s.loads(token, max_age=max_age) + token_data = s.loads(token) + try: + expire = datetime.fromisoformat(token_data.get("expire")) + except (ValueError, TypeError): + raise BadSignature("Invalid token") + + if expire < datetime.now(timezone.utc): + raise SignatureExpired("Token expired") + + return token_data def encode_token(secret_key, salt, data): diff --git a/server/mergin/tests/test_auth.py b/server/mergin/tests/test_auth.py index 3a4c2de4..90777122 100644 --- a/server/mergin/tests/test_auth.py +++ b/server/mergin/tests/test_auth.py @@ -2,14 +2,16 @@ # # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import time +import itsdangerous import pytest import json from flask import url_for from sqlalchemy import desc from unittest.mock import patch +from ..auth.bearer import decode_token, encode_token from ..auth.forms import ResetPasswordForm from ..auth.app import generate_confirmation_token, confirm_token from ..auth.models import User, UserProfile, LoginHistory @@ -958,3 +960,57 @@ def test_login_without_password(client): headers=json_headers, ) assert resp.status_code == 401 + + +def test_bearer_token_expiration(app): + """Test bearer token expiration in decode function""" + secret = app.config["SECRET_KEY"] + salt = app.config["SECURITY_BEARER_SALT"] + client = app.test_client() + user = User.query.first() + + # valid token with longer expiration then usual + expire = datetime.now(timezone.utc) + timedelta( + seconds=2 * app.config["BEARER_TOKEN_EXPIRATION"] + ) + token_data = { + "user_id": user.id, + "username": user.username, + "email": user.email, + "expire": str(expire), + } + + token = encode_token(secret, salt, token_data) + data = decode_token(secret, salt, token) + assert data["expire"] == str(expire) + + # try to login + resp = client.get( + "/v1/user/profile", headers={**json_headers, "Authorization": f"Bearer {token}"} + ) + assert resp.status_code == 200 + client.get(url_for("/.mergin_auth_controller_logout")) + + # expired token + expire = datetime.now(timezone.utc) - timedelta(days=1) + token_data["expire"] = str(expire) + token = encode_token(secret, salt, token_data) + + with pytest.raises(itsdangerous.exc.SignatureExpired): + decode_token(secret, salt, token) + + resp = client.get( + "/v1/user/profile", headers={**json_headers, "Authorization": f"Bearer {token}"} + ) + assert resp.status_code == 401 + + # invalid token + token_data["expire"] = 123 + token = encode_token(secret, salt, token_data) + with pytest.raises(itsdangerous.exc.BadSignature): + decode_token(secret, salt, token) + + resp = client.get( + "/v1/user/profile", headers={**json_headers, "Authorization": f"Bearer {token}"} + ) + assert resp.status_code == 401 From 9b6de9d4da56eda76955870d57f704dfb4c77a97 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Fri, 4 Jul 2025 10:07:13 +0200 Subject: [PATCH 2/2] Bump 2025.5.1 --- deployment/enterprise/docker-compose.yml | 8 ++++---- server/mergin/version.py | 2 +- server/setup.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/deployment/enterprise/docker-compose.yml b/deployment/enterprise/docker-compose.yml index 95cb1657..c4524707 100644 --- a/deployment/enterprise/docker-compose.yml +++ b/deployment/enterprise/docker-compose.yml @@ -5,7 +5,7 @@ networks: services: server: - image: 433835555346.dkr.ecr.eu-west-1.amazonaws.com/mergin/mergin-ee-back:2025.5.0 + image: 433835555346.dkr.ecr.eu-west-1.amazonaws.com/mergin/mergin-ee-back:2025.5.1 container_name: merginmaps-server restart: always user: 901:999 @@ -21,7 +21,7 @@ services: networks: - mergin web: - image: 433835555346.dkr.ecr.eu-west-1.amazonaws.com/mergin/mergin-ee-front:2025.5.0 + image: 433835555346.dkr.ecr.eu-west-1.amazonaws.com/mergin/mergin-ee-front:2025.5.1 container_name: merginmaps-web restart: always depends_on: @@ -51,7 +51,7 @@ services: - server celery-beat: - image: 433835555346.dkr.ecr.eu-west-1.amazonaws.com/mergin/mergin-ee-back:2025.5.0 + image: 433835555346.dkr.ecr.eu-west-1.amazonaws.com/mergin/mergin-ee-back:2025.5.1 container_name: merginmaps-celery-beat restart: always user: 901:999 @@ -67,7 +67,7 @@ services: - mergin celery-worker: - image: 433835555346.dkr.ecr.eu-west-1.amazonaws.com/mergin/mergin-ee-back:2025.5.0 + image: 433835555346.dkr.ecr.eu-west-1.amazonaws.com/mergin/mergin-ee-back:2025.5.1 container_name: merginmaps-celery-worker restart: always user: 901:999 diff --git a/server/mergin/version.py b/server/mergin/version.py index 818a707c..694adecc 100644 --- a/server/mergin/version.py +++ b/server/mergin/version.py @@ -4,4 +4,4 @@ def get_version(): - return "2025.5.0" + return "2025.5.1" diff --git a/server/setup.py b/server/setup.py index eefc21aa..3d001842 100644 --- a/server/setup.py +++ b/server/setup.py @@ -6,7 +6,7 @@ setup( name="mergin", - version="2025.5.0", + version="2025.5.1", url="https://github.com/MerginMaps/mergin", license="AGPL-3.0-only", author="Lutra Consulting Limited",