From dbaa9cf6da2278dca0d06ffbc876c66265f96379 Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Mon, 12 Oct 2026 14:00:50 +0100 Subject: [PATCH 1/2] fix(identity): add auth_type and resource to AuthBlockingEvent Derived from the blocking token the same way as the Node SDK, so code written from the blocking functions docs works on Python. --- src/firebase_functions/identity_fn.py | 31 +++++++++ .../private/_identity_fn.py | 14 +++- tests/test_identity_fn.py | 69 +++++++++++++++++++ 3 files changed, 113 insertions(+), 1 deletion(-) diff --git a/src/firebase_functions/identity_fn.py b/src/firebase_functions/identity_fn.py index d2afead1..9af0f784 100644 --- a/src/firebase_functions/identity_fn.py +++ b/src/firebase_functions/identity_fn.py @@ -255,6 +255,31 @@ class SmsType(str, Enum): MULTI_FACTOR_ENROLLMENT = "MULTI_FACTOR_ENROLLMENT" +AuthType = _typing.Literal["USER", "UNAUTHENTICATED"] +""" +Whether the event was triggered by a user or by an unauthenticated request. +""" + + +@_dataclasses.dataclass(frozen=True) +class AuthBlockingResource: + """ + The resource that emitted an auth blocking event. + """ + + service: str + """ + The service that emitted the event. + Example: 'identitytoolkit.googleapis.com' + """ + + name: str + """ + The project, or the tenant within it, the event belongs to. + Example: 'projects/my-project' or 'projects/my-project/tenants/my-tenant' + """ + + @_dataclasses.dataclass(frozen=True) class AuthBlockingEvent: """ @@ -305,6 +330,12 @@ class AuthBlockingEvent: sms_type: SmsType | None """The type of SMS event.""" + auth_type: AuthType + """Whether the event was triggered by a user or by an unauthenticated request.""" + + resource: AuthBlockingResource + """The resource that emitted the event.""" + timestamp: _dt.datetime """ The time the event was triggered.""" diff --git a/src/firebase_functions/private/_identity_fn.py b/src/firebase_functions/private/_identity_fn.py index 0e09c04a..1f3a3080 100644 --- a/src/firebase_functions/private/_identity_fn.py +++ b/src/firebase_functions/private/_identity_fn.py @@ -19,6 +19,7 @@ import time as _time import typing as _typing +import firebase_admin as _firebase_admin from flask import ( Request as _Request, ) @@ -215,7 +216,13 @@ def _credential_from_token_data(token_data: dict[str, _typing.Any], time: float) def _auth_blocking_event_from_token_data(event_type: str, token_data: dict[str, _typing.Any]): - from firebase_functions.identity_fn import AuthBlockingEvent + from firebase_functions.identity_fn import AuthBlockingEvent, AuthBlockingResource + + project_id = _firebase_admin.get_app().project_id + tenant_id = token_data.get("tenant_id") + resource_name = ( + f"projects/{project_id}/tenants/{tenant_id}" if tenant_id else f"projects/{project_id}" + ) return AuthBlockingEvent( data=_auth_user_record_from_token_data(token_data["user_record"]), @@ -229,6 +236,11 @@ def _auth_blocking_event_from_token_data(event_type: str, token_data: dict[str, credential=_credential_from_token_data(token_data, _time.time()), email_type=token_data.get("email_type"), sms_type=token_data.get("sms_type"), + auth_type="USER" if token_data.get("user_record") else "UNAUTHENTICATED", + resource=AuthBlockingResource( + service="identitytoolkit.googleapis.com", + name=resource_name, + ), ) diff --git a/tests/test_identity_fn.py b/tests/test_identity_fn.py index 19421a3d..c3a66730 100644 --- a/tests/test_identity_fn.py +++ b/tests/test_identity_fn.py @@ -53,3 +53,72 @@ def init(): decorated_func(request) self.assertEqual("world", hello) + + def test_handler_receives_auth_type_and_resource(self): + received = None + + def func(event): + nonlocal received + received = event + + with ( + patch.object(_identity_fn, "_token_verifier", token_verifier_mock), + patch.object(_identity_fn._firebase_admin, "get_app", return_value=app_mock), + ): + app = Flask(__name__) + with app.test_request_context("/"): + environ = EnvironBuilder( + method="POST", + json={"data": {"jwt": "jwt"}}, + ).get_environ() + response = identity_fn.before_user_signed_in()(func)(Request(environ)) + + self.assertEqual(200, response.status_code) + self.assertEqual("USER", received.auth_type) + self.assertEqual( + identity_fn.AuthBlockingResource( + service="identitytoolkit.googleapis.com", + name="projects/project-id", + ), + received.resource, + ) + + +app_mock = Mock(project_id="project-id") + + +class TestAuthBlockingEventFromTokenData(unittest.TestCase): + """ + Tests for building an AuthBlockingEvent from a decoded blocking token. + """ + + def _event(self, **token_fields): + token_data = { + **token_verifier_mock.verify_auth_blocking_token.return_value, + **token_fields, + } + with patch.object(_identity_fn._firebase_admin, "get_app", return_value=app_mock): + return _identity_fn._auth_blocking_event_from_token_data( + _identity_fn.event_type_before_sign_in, token_data + ) + + def test_auth_type_is_user_when_token_has_user_record(self): + self.assertEqual("USER", self._event().auth_type) + + def test_resource_names_project_when_token_has_no_tenant(self): + self.assertEqual( + identity_fn.AuthBlockingResource( + service="identitytoolkit.googleapis.com", + name="projects/project-id", + ), + self._event().resource, + ) + + def test_resource_names_tenant_when_token_has_tenant_id(self): + self.assertEqual( + identity_fn.AuthBlockingResource( + service="identitytoolkit.googleapis.com", + name="projects/project-id/tenants/tenant-1", + ), + self._event(tenant_id="tenant-1").resource, + ) From 607af7c8ae7428049d45e05d84162638b97c58fe Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Mon, 12 Oct 2026 14:16:21 +0100 Subject: [PATCH 2/2] test(identity): mock get_app in test_calls_init_function --- tests/test_identity_fn.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_identity_fn.py b/tests/test_identity_fn.py index c3a66730..a35166b2 100644 --- a/tests/test_identity_fn.py +++ b/tests/test_identity_fn.py @@ -36,7 +36,10 @@ def init(): nonlocal hello hello = "world" - with patch.object(_identity_fn, "_token_verifier", token_verifier_mock): + with ( + patch.object(_identity_fn, "_token_verifier", token_verifier_mock), + patch.object(_identity_fn._firebase_admin, "get_app", return_value=app_mock), + ): app = Flask(__name__) func = Mock(__name__="example_func", return_value=identity_fn.BeforeSignInResponse())