From dbaa9cf6da2278dca0d06ffbc876c66265f96379 Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Mon, 12 Oct 2026 14:00:50 +0100 Subject: [PATCH 1/4] 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/4] 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()) From dd56968131d8135436e271882627de5652684a22 Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Wed, 4 Nov 2026 13:02:19 +0000 Subject: [PATCH 3/4] test(identity): patch get_app in timezone-aware blocking event test --- tests/test_identity_fn.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_identity_fn.py b/tests/test_identity_fn.py index 0ec577ad..af9f7a38 100644 --- a/tests/test_identity_fn.py +++ b/tests/test_identity_fn.py @@ -515,6 +515,7 @@ def example( with ( patch.object(_identity_fn, "_token_verifier", mock_verifier), + patch.object(_identity_fn._firebase_admin, "get_app", return_value=app_mock), patch.object(_identity_fn._time, "time", return_value=1672963200.0), ): app = Flask(__name__) @@ -641,6 +642,7 @@ def on_before_create( with ( patch.object(_identity_fn, "_token_verifier", mock_verifier), + patch.object(_identity_fn._firebase_admin, "get_app", return_value=app_mock), patch.object(_identity_fn._time, "time", return_value=1672963200.0), ): app = Flask(__name__) From f8293a418a7ea51ac65b5e4af300da83ca750a77 Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Thu, 5 Nov 2026 10:36:54 +0000 Subject: [PATCH 4/4] fix(identity): read the project ID for AuthBlockingEvent.resource from GCLOUD_PROJECT Matches the Node SDK. --- src/firebase_functions/private/_identity_fn.py | 13 +++++++++---- tests/test_identity_fn.py | 16 +++++----------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/firebase_functions/private/_identity_fn.py b/src/firebase_functions/private/_identity_fn.py index e90cf8ce..2d77ee78 100644 --- a/src/firebase_functions/private/_identity_fn.py +++ b/src/firebase_functions/private/_identity_fn.py @@ -16,12 +16,12 @@ # pylint: disable=protected-access import datetime as _dt import json as _json +import os as _os import re as _re import time as _time import typing as _typing import cloudevents.http as _ce -import firebase_admin as _firebase_admin from flask import ( Request as _Request, ) @@ -230,10 +230,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]): +def _auth_blocking_event_from_token_data( + event_type: str, + token_data: dict[str, _typing.Any], + project_id: str | None, +): 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}" @@ -381,7 +384,9 @@ def before_operation_handler( raise HttpsError(FunctionsErrorCode.INVALID_ARGUMENT, "Bad Request") jwt_token = request.json["data"]["jwt"] decoded_token = _token_verifier.verify_auth_blocking_token(jwt_token) - event = _auth_blocking_event_from_token_data(event_type, decoded_token) + event = _auth_blocking_event_from_token_data( + event_type, decoded_token, _os.environ.get("GCLOUD_PROJECT") + ) auth_response: BeforeCreateResponse | BeforeSignInResponse | None = _with_init(func)(event) if not auth_response: return _jsonify({}) diff --git a/tests/test_identity_fn.py b/tests/test_identity_fn.py index af9f7a38..1cab9b8a 100644 --- a/tests/test_identity_fn.py +++ b/tests/test_identity_fn.py @@ -3,6 +3,7 @@ """ import datetime as dt +import os import unittest from unittest.mock import MagicMock, Mock, patch @@ -45,7 +46,6 @@ def init(): 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__) @@ -515,7 +515,6 @@ def example( with ( patch.object(_identity_fn, "_token_verifier", mock_verifier), - patch.object(_identity_fn._firebase_admin, "get_app", return_value=app_mock), patch.object(_identity_fn._time, "time", return_value=1672963200.0), ): app = Flask(__name__) @@ -642,7 +641,6 @@ def on_before_create( with ( patch.object(_identity_fn, "_token_verifier", mock_verifier), - patch.object(_identity_fn._firebase_admin, "get_app", return_value=app_mock), patch.object(_identity_fn._time, "time", return_value=1672963200.0), ): app = Flask(__name__) @@ -682,7 +680,7 @@ def func(event): with ( patch.object(_identity_fn, "_token_verifier", token_verifier_mock), - patch.object(_identity_fn._firebase_admin, "get_app", return_value=app_mock), + patch.dict(os.environ, {"GCLOUD_PROJECT": "project-id"}), ): app = Flask(__name__) with app.test_request_context("/"): @@ -703,9 +701,6 @@ def func(event): ) -app_mock = Mock(project_id="project-id") - - class TestAuthBlockingEventFromTokenData(unittest.TestCase): """ Tests for building an AuthBlockingEvent from a decoded blocking token. @@ -716,10 +711,9 @@ def _event(self, **token_fields): **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 - ) + return _identity_fn._auth_blocking_event_from_token_data( + _identity_fn.event_type_before_sign_in, token_data, "project-id" + ) def test_auth_type_is_user_when_token_has_user_record(self): self.assertEqual("USER", self._event().auth_type)