Skip to content

Commit b69e710

Browse files
alfonsosastreAlfonso Sastrecursoragent
authored
fix: auth token base64 fallback and CiString case preservation (#9)
* fix: auth token base64 fallback and CiString case preservation - Make base64 token decoding resilient: catch all decode errors and fall back to raw token instead of raising AuthorizationOCPIError. This allows development setups to use plain-text tokens with OCPI 2.2+ endpoints. - Preserve original case in CiString instead of forcing lower/upper. CiString means case-insensitive comparison, not mutation. Forcing case destroyed identifiers like OCPP charge point IDs. - Change CI_STRING_LOWERCASE_PREFERENCE default to false (preserve case by default). - Add try/except in get_auth_token() for decode_string_base64 to gracefully handle non-base64 tokens. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: add auth_id to Session and CDR schemas (OCPI 2.2.1 required field) Co-authored-by: Cursor <cursoragent@cursor.com> * fix: auth token fallback, CiString case preservation, remove auth_id (OCPI 2.2.1) - Auth: fallback to raw token when base64 decode fails (dev/integration) - CiString: preserve original case (OCPP IDs like K0032832A) - Remove auth_id from Session/CDR v2.2.1 (replaced by CdrToken per spec) - Remove CI_STRING_LOWERCASE_PREFERENCE (dead config) - Narrow exception handling to (UnicodeDecodeError, ValueError) - Add tests for plain-text fallback and CiString case preservation Co-authored-by: Cursor <cursoragent@cursor.com> * style: ruff format verifier.py Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Alfonso Sastre <alfonso@elumobility.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent c08689d commit b69e710

7 files changed

Lines changed: 44 additions & 36 deletions

File tree

ocpi/core/authentication/verifier.py

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,10 @@ async def __call__(
6565
from ocpi.core.utils import decode_string_base64
6666

6767
token = decode_string_base64(token)
68-
except UnicodeDecodeError:
69-
logger.debug(
70-
f"Token `{token}` cannot be decoded. "
71-
"Check if the token is already encoded."
72-
)
73-
raise AuthorizationOCPIError
68+
except (UnicodeDecodeError, ValueError) as e:
69+
# If base64 decoding fails (bad padding, invalid chars),
70+
# try authenticating with the raw token as fallback.
71+
logger.debug(f"Token base64 decode failed ({e}), trying raw token.")
7472
await authenticator.authenticate(token)
7573
except IndexError:
7674
logger.debug(
@@ -122,19 +120,15 @@ async def __call__(
122120
from ocpi.core.utils import decode_string_base64
123121

124122
token = decode_string_base64(token)
125-
except UnicodeDecodeError:
126-
logger.debug(
127-
f"Token `{token}` cannot be decoded. "
128-
"Check if the token is already encoded."
129-
)
130-
raise AuthorizationOCPIError
123+
except (UnicodeDecodeError, ValueError) as e:
124+
logger.debug(f"Token base64 decode failed ({e}), trying raw token.")
131125
else:
132126
# For versions without explicit version (legacy), try to decode
133127
try:
134128
from ocpi.core.utils import decode_string_base64
135129

136130
token = decode_string_base64(token)
137-
except UnicodeDecodeError:
131+
except (UnicodeDecodeError, ValueError):
138132
pass
139133
return await authenticator.authenticate_credentials(token)
140134

@@ -205,12 +199,8 @@ async def __call__(
205199
from ocpi.core.utils import decode_string_base64
206200

207201
token = decode_string_base64(token)
208-
except UnicodeDecodeError:
209-
logger.debug(
210-
f"Token `{token}` cannot be decoded. "
211-
"Check if the token is already encoded."
212-
)
213-
raise AuthorizationOCPIError
202+
except (UnicodeDecodeError, ValueError) as e:
203+
logger.debug(f"Token base64 decode failed ({e}), trying raw token.")
214204
await authenticator.authenticate(token)
215205
except IndexError:
216206
logger.debug(
@@ -258,12 +248,8 @@ async def __call__(
258248
from ocpi.core.utils import decode_string_base64
259249

260250
token = decode_string_base64(token)
261-
except UnicodeDecodeError:
262-
logger.debug(
263-
f"Token `{token}` cannot be decoded. "
264-
"Check if the token is already encoded."
265-
)
266-
raise AuthorizationOCPIError
251+
except (UnicodeDecodeError, ValueError) as e:
252+
logger.debug(f"Token base64 decode failed ({e}), trying raw token.")
267253
await authenticator.authenticate(token)
268254
except AuthorizationOCPIError:
269255
raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION)

ocpi/core/config.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ class Settings(BaseSettings):
3030
COMMAND_AWAIT_TIME: int = 5
3131
GET_ACTIVE_PROFILE_AWAIT_TIME: int = 5
3232
TRAILING_SLASH: bool = True
33-
CI_STRING_LOWERCASE_PREFERENCE: bool = True
3433

3534
@field_validator("BACKEND_CORS_ORIGINS", mode="before")
3635
@classmethod

ocpi/core/data_types.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
from pydantic.json_schema import JsonSchemaValue
1212
from pydantic_core import CoreSchema, core_schema
1313

14-
from .config import settings
15-
1614

1715
class StringBase(str):
1816
"""
@@ -71,9 +69,10 @@ def __get_pydantic_json_schema__(
7169
def _validate(cls, v: str) -> "CiStringBase":
7270
if not v.isascii():
7371
raise ValueError("invalid cistring format")
74-
if settings.CI_STRING_LOWERCASE_PREFERENCE:
75-
return cls(v.lower())
76-
return cls(v.upper())
72+
# Preserve original case. CiString means case-insensitive *comparison*,
73+
# not that the value should be mutated. Lowercasing/uppercasing destroys
74+
# identifiers like OCPP charge point IDs (e.g. "K0032832A").
75+
return cls(v)
7776

7877

7978
class URL(str):

ocpi/core/utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ def get_auth_token(
2929
return None
3030
if version.startswith("2.1") or version.startswith("2.0"):
3131
return token
32-
return decode_string_base64(token)
32+
try:
33+
return decode_string_base64(token)
34+
except (UnicodeDecodeError, ValueError):
35+
# Fallback: if base64 decoding fails (e.g. raw token in dev),
36+
# return the raw token.
37+
return token
3338

3439

3540
async def get_list(

tests/test_core/test_data_types.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,13 @@ def test_cistring_base_valid():
6262
"""Test CiStringBase with valid ASCII string."""
6363
result = CiStringBase("TEST")
6464
assert isinstance(result, CiStringBase)
65-
# Should be uppercase or lowercase based on settings
66-
assert result in ["TEST", "test"]
65+
assert result == "TEST"
66+
67+
68+
def test_cistring_base_preserves_case():
69+
"""Test CiStringBase preserves original case (OCPP charge point IDs, etc.)."""
70+
result = CiStringBase("K0032832A")
71+
assert result == "K0032832A"
6772

6873

6974
def test_cistring_factory():

tests/test_core/test_utils.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,20 @@ def test_get_auth_token_v2_2_1_base64():
5858
assert token == original_token
5959

6060

61+
def test_get_auth_token_v2_2_1_plain_text_fallback():
62+
"""Test get_auth_token with OCPI 2.2.1 falls back to raw token when base64 decode fails."""
63+
from unittest.mock import MagicMock
64+
65+
# Plain-text token (not base64) - e.g. dev token
66+
raw_token = "plain-dev-token"
67+
68+
request = MagicMock(spec=Request)
69+
request.headers = {"authorization": f"Token {raw_token}"}
70+
71+
token = get_auth_token(request, VersionNumber.v_2_2_1)
72+
assert token == raw_token
73+
74+
6175
def test_get_auth_token_null():
6276
"""Test get_auth_token with Null token returns None."""
6377
from unittest.mock import MagicMock

tests/test_modules/test_v_2_3_0/test_hubclientinfo/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
WRONG_AUTH_HEADERS = {"Authorization": f"Token {ENCODED_RANDOM_AUTH_TOKEN_V_2_3_0}"}
1212

1313
HUB_CLIENT_INFO = {
14-
"party_id": "aaa", # CiString normalizes to lowercase
15-
"country_code": "us", # CiString normalizes to lowercase
14+
"party_id": "aaa",
15+
"country_code": "us",
1616
"role": enums.RoleEnum.cpo,
1717
"status": ConnectionStatus.connected,
1818
"last_updated": "2022-01-02 00:00:00+00:00",

0 commit comments

Comments
 (0)