Skip to content

Commit 9ea1349

Browse files
fix(identity): validate metadata timestamps and reject empty tenant_id
1 parent 0845bff commit 9ea1349

3 files changed

Lines changed: 46 additions & 20 deletions

File tree

‎src/firebase_functions/options.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,8 @@ def _endpoint(
10741074
assert kwargs["event_type"] is not None
10751075
event_filters: dict[str, str | Expression[str]] = {}
10761076
if self.tenant_id is not None:
1077+
if self.tenant_id == "":
1078+
raise ValueError("AuthOptions: tenant_id cannot be an empty string.")
10771079
event_filters["tenantid"] = self.tenant_id
10781080

10791081
endpoint = super()._endpoint(

‎src/firebase_functions/private/_identity_fn.py‎

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -403,11 +403,10 @@ def _parse_auth_timestamp(value: _typing.Any) -> _dt.datetime | None:
403403
except (OverflowError, OSError, ValueError):
404404
return None
405405
if isinstance(value, str) and value:
406-
for parser in (_util.timestamp_conversion, _dt.datetime.fromisoformat):
407-
try:
408-
return _ensure_utc(parser(value))
409-
except ValueError:
410-
continue
406+
try:
407+
return _ensure_utc(_util.timestamp_conversion(value))
408+
except ValueError:
409+
return None
411410
return None
412411

413412

@@ -442,10 +441,24 @@ def _auth_user_metadata_from_ce_data(meta_data: dict[str, _typing.Any] | None):
442441

443442
# Production Eventarc sends 'createTime'; Auth Emulator sends 'creationTime'.
444443
raw_creation = meta_data.get("createTime") or meta_data.get("creationTime")
444+
if raw_creation not in (None, ""):
445+
creation_time = _parse_auth_timestamp(raw_creation)
446+
if creation_time is None:
447+
raise ValueError(f"Invalid Auth metadata creationTime: {raw_creation!r}")
448+
else:
449+
creation_time = _EPOCH_START_UTC
450+
445451
raw_last_sign_in = meta_data.get("lastSignInTime")
452+
if raw_last_sign_in not in (None, ""):
453+
last_sign_in_time = _parse_auth_timestamp(raw_last_sign_in)
454+
if last_sign_in_time is None:
455+
raise ValueError(f"Invalid Auth metadata lastSignInTime: {raw_last_sign_in!r}")
456+
else:
457+
last_sign_in_time = None
458+
446459
return AuthUserMetadata(
447-
creation_time=_parse_auth_timestamp(raw_creation) or _EPOCH_START_UTC,
448-
last_sign_in_time=_parse_auth_timestamp(raw_last_sign_in),
460+
creation_time=creation_time,
461+
last_sign_in_time=last_sign_in_time,
449462
)
450463

451464

@@ -457,13 +470,11 @@ def _unwrap_auth_event_data(raw_data: _typing.Any) -> dict[str, _typing.Any]:
457470
if not raw_data or not isinstance(raw_data, dict):
458471
raise ValueError("Auth event data payload must be a non-empty dictionary.")
459472
# 'on_user_created' wraps the user in 'value'; 'on_user_deleted' wraps it in 'oldValue'.
460-
# Check isinstance(..., dict) so {'value': None, 'oldValue': {...}} does not shadow 'oldValue'.
461-
for envelope_key in ("value", "oldValue"):
462-
if isinstance(raw_data.get(envelope_key), dict) and raw_data[envelope_key]:
463-
return raw_data[envelope_key]
464-
# If either envelope key is present with a non-dict (e.g. None) or empty dict, reject it.
465-
if any(k in raw_data for k in ("value", "oldValue")):
466-
raise ValueError("Auth event data envelope contains an empty or null user record.")
473+
if "value" in raw_data or "oldValue" in raw_data:
474+
user_data = raw_data.get("value") or raw_data.get("oldValue")
475+
if not isinstance(user_data, dict) or not user_data:
476+
raise ValueError("Auth event data envelope contains an empty or null user record.")
477+
return user_data
467478
# Otherwise, assume raw_data is already the top-level user dict (e.g. synthetic tests).
468479
return raw_data
469480

@@ -529,12 +540,9 @@ def _auth_event_from_ce(raw: _ce.CloudEvent):
529540

530541
# Extract project ID from the CloudEvent source attribute
531542
# (e.g. "//identitytoolkit.googleapis.com/projects/my-project").
532-
source = event_attributes.get("source", "")
533-
project: str | None = None
534-
if source:
535-
match = _PROJECT_ID_REGEX.search(source)
536-
if match:
537-
project = match.group(1)
543+
source: str = event_attributes["source"]
544+
match = _PROJECT_ID_REGEX.search(source)
545+
project: str | None = match.group(1) if match else None
538546

539547
raw_time = event_attributes.get("time")
540548
event_time = _parse_auth_timestamp(raw_time)

‎tests/test_identity_fn.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ def example(_event: identity_fn.AuthEvent) -> None:
108108
assert endpoint is not None
109109
self.assertEqual(endpoint.eventTrigger["eventFilters"], {"tenantid": "my-tenant"})
110110

111+
with self.assertRaises(ValueError):
112+
113+
@identity_fn.on_user_created(tenant_id="")
114+
def _empty_tenant(_event: identity_fn.AuthEvent) -> None:
115+
pass
116+
111117
def test_on_user_created_tenant_id_expression(self) -> None:
112118
tenant_param = params.StringParam("MY_TENANT")
113119

@@ -274,6 +280,16 @@ def example(event: identity_fn.AuthEvent) -> None:
274280
{"value": {}},
275281
{"unrecognized": "payload"},
276282
{"value": {"email": "missing-uid@example.com"}},
283+
{"value": {"uid": "valid-uid", "metadata": {"createTime": "not-a-timestamp"}}},
284+
{
285+
"value": {
286+
"uid": "valid-uid",
287+
"metadata": {
288+
"createTime": "2023-01-01T00:00:00Z",
289+
"lastSignInTime": "not-a-timestamp",
290+
},
291+
}
292+
},
277293
]
278294
for invalid_data in invalid_payloads:
279295
raw_invalid_ce = CloudEvent(

0 commit comments

Comments
 (0)