Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/firebase_functions/identity_fn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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."""
Comment thread
IzaakGough marked this conversation as resolved.

resource: AuthBlockingResource
"""The resource that emitted the event."""

timestamp: _dt.datetime
"""
The time the event was triggered."""
Expand Down
14 changes: 13 additions & 1 deletion src/firebase_functions/private/_identity_fn.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import time as _time
import typing as _typing

import firebase_admin as _firebase_admin
from flask import (
Request as _Request,
)
Expand Down Expand Up @@ -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"]),
Expand All @@ -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",
Comment thread
IzaakGough marked this conversation as resolved.
resource=AuthBlockingResource(
service="identitytoolkit.googleapis.com",
name=resource_name,
),
)


Expand Down
74 changes: 73 additions & 1 deletion tests/test_identity_fn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -53,3 +56,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,
)
Loading