From 2609c83a0e858dbedc3bd7e6d7e66bbdf819a2a8 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Fri, 22 Aug 2025 10:55:50 +0200 Subject: [PATCH 1/2] Do not expect tokens to start with dot --- mergin/client.py | 23 +++++++++++++++++------ mergin/test/test_client.py | 23 ++++++++++++++++++++++- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/mergin/client.py b/mergin/client.py index e9d0b21e..8aeace10 100644 --- a/mergin/client.py +++ b/mergin/client.py @@ -62,15 +62,26 @@ class ServerType(Enum): def decode_token_data(token): - token_prefix = "Bearer ." + token_prefix = "Bearer " if not token.startswith(token_prefix): - raise TokenError(f"Token doesn't start with 'Bearer .': {token}") + raise TokenError(f"Token doesn't start with 'Bearer ': {token}") try: - data = token[len(token_prefix) :].split(".")[0] - # add proper base64 padding" + data = token[len(token_prefix) :] + + if data.startswith('.'): + data = data.lstrip(".") + + data = data.split(".")[0] + # add proper base64 padding data += "=" * (-len(data) % 4) - decoded = zlib.decompress(base64.urlsafe_b64decode(data)) - return json.loads(decoded) + data = base64.urlsafe_b64decode(data) + + try: + data = zlib.decompress(data) + except zlib.error as e: + print("There was an issue during decompression, continuing without it, error:", e) + + return json.loads(data) except (IndexError, TypeError, ValueError, zlib.error): raise TokenError(f"Invalid token data: {token}") diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index 32bc192f..811668b9 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -152,15 +152,36 @@ def test_login(mc): assert MerginClient(mc.url, auth_token=token) invalid_token = "Completely invalid token...." - with pytest.raises(TokenError, match=f"Token doesn't start with 'Bearer .': {invalid_token}"): + with pytest.raises(TokenError, match=f"Token doesn't start with 'Bearer ': {invalid_token}"): decode_token_data(invalid_token) invalid_token = "Bearer .jas646kgfa" with pytest.raises(TokenError, match=f"Invalid token data: {invalid_token}"): decode_token_data(invalid_token) + invalid_token = "Bearer jas646kgfa" + with pytest.raises(TokenError, match=f"Invalid token data: {invalid_token}"): + decode_token_data(invalid_token) + with pytest.raises(LoginError, match="Invalid username or password"): mc.login("foo", "bar") + + valid_token_dot = "Bearer .eJxNi0kKgDAMAL8iubqQRuuSkz-RojkEWi0uIIh_Fz15G2aYC45N1kEnYJN9PLsgwOCijl5l3iEDCU793_VyuhC9FOMS3n5GXd-JkGyObU6UmI6pZoNFZRtbUorIiHA_KFshoA.abc.def" + decoded_value = decode_token_data(valid_token_dot) + + # expected: {'user_id': 1, 'username': 'apiclient', 'email': 'apiclient@example.com', 'expire': '2025-08-22 19:26:10.457532+00:00'} + assert decoded_value["user_id"] == 1 + assert decoded_value["username"] == "apiclient" + assert decoded_value["email"] == "apiclient@example.com" + assert decoded_value["expire"] == "2025-08-22 19:26:10.457532+00:00" + + valid_token_nodot = "Bearer eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFwaSIsImVtYWlsIjoiYXBpQGUuY29tIn0.abc.def" + decoded_value = decode_token_data(valid_token_nodot) + + # expected: {'user_id': 1, 'username': 'api', 'email': 'api@e.com'} + assert decoded_value["user_id"] == 1 + assert decoded_value["username"] == "api" + assert decoded_value["email"] == "api@e.com" def test_create_delete_project(mc: MerginClient): From 5bf360a00e696ba9f056c302765a31923d7891c2 Mon Sep 17 00:00:00 2001 From: Tomas Mizera Date: Tue, 26 Aug 2025 10:43:54 +0200 Subject: [PATCH 2/2] Compress only tokens that start with dot --- mergin/client.py | 28 ++++++++++++++++------------ mergin/test/test_client.py | 2 +- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/mergin/client.py b/mergin/client.py index 8aeace10..75f34026 100644 --- a/mergin/client.py +++ b/mergin/client.py @@ -66,22 +66,26 @@ def decode_token_data(token): if not token.startswith(token_prefix): raise TokenError(f"Token doesn't start with 'Bearer ': {token}") try: - data = token[len(token_prefix) :] - - if data.startswith('.'): - data = data.lstrip(".") + token_raw = token[len(token_prefix) :] + is_compressed = False + + # compressed tokens start with dot, + # see https://github.com/pallets/itsdangerous/blob/main/src/itsdangerous/url_safe.py#L55 + if token_raw.startswith("."): + token_raw = token_raw.lstrip(".") + is_compressed = True + + payload_raw = token_raw.split(".")[0] - data = data.split(".")[0] # add proper base64 padding - data += "=" * (-len(data) % 4) - data = base64.urlsafe_b64decode(data) + payload_raw += "=" * (-len(payload_raw) % 4) + payload_data = base64.urlsafe_b64decode(payload_raw) - try: - data = zlib.decompress(data) - except zlib.error as e: - print("There was an issue during decompression, continuing without it, error:", e) + if is_compressed: + payload_data = zlib.decompress(payload_data) + + return json.loads(payload_data) - return json.loads(data) except (IndexError, TypeError, ValueError, zlib.error): raise TokenError(f"Invalid token data: {token}") diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index 811668b9..43263929 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -165,7 +165,7 @@ def test_login(mc): with pytest.raises(LoginError, match="Invalid username or password"): mc.login("foo", "bar") - + valid_token_dot = "Bearer .eJxNi0kKgDAMAL8iubqQRuuSkz-RojkEWi0uIIh_Fz15G2aYC45N1kEnYJN9PLsgwOCijl5l3iEDCU793_VyuhC9FOMS3n5GXd-JkGyObU6UmI6pZoNFZRtbUorIiHA_KFshoA.abc.def" decoded_value = decode_token_data(valid_token_dot)