Skip to content

Commit f9b8ae5

Browse files
committed
feat: Implement before_email_sent and before_sms_sent blocking functions
1 parent f516594 commit f9b8ae5

File tree

3 files changed

+182
-15
lines changed

3 files changed

+182
-15
lines changed

src/firebase_functions/identity_fn.py

+151-2
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ class AdditionalUserInfo:
191191
The additional user info component of the auth event context.
192192
"""
193193

194-
provider_id: str
194+
provider_id: str | None
195195
"""The provider identifier."""
196196

197197
profile: dict[str, _typing.Any] | None
@@ -206,6 +206,12 @@ class AdditionalUserInfo:
206206
recaptcha_score: float | None
207207
"""The user's reCAPTCHA score, if available."""
208208

209+
email: str | None
210+
"""The user's email, if available."""
211+
212+
phone_number: str | None
213+
"""The user's phone number, if available."""
214+
209215

210216
@_dataclasses.dataclass(frozen=True)
211217
class Credential:
@@ -237,14 +243,16 @@ class Credential:
237243
sign_in_method: str
238244
"""The user's sign-in method."""
239245

246+
EmailType = _typing.Literal["EMAIL_SIGN_IN", "PASSWORD_RESET"]
247+
SmsType = _typing.Literal["SIGN_IN_OR_SIGN_UP", "MULTI_FACTOR_SIGN_IN", "MULTI_FACTOR_ENROLLMENT"]
240248

241249
@_dataclasses.dataclass(frozen=True)
242250
class AuthBlockingEvent:
243251
"""
244252
Defines an auth event for identitytoolkit v2 auth blocking events.
245253
"""
246254

247-
data: AuthUserRecord
255+
data: AuthUserRecord | None # This is None for beforeEmailSent and beforeSmsSent events
248256
"""
249257
The UserRecord passed to auth blocking functions from the identity platform.
250258
"""
@@ -280,6 +288,12 @@ class AuthBlockingEvent:
280288
credential: Credential | None
281289
"""An object containing information about the user's credential."""
282290

291+
email_type: EmailType | None
292+
"""The type of email event."""
293+
294+
sms_type: SmsType | None
295+
"""The type of SMS event."""
296+
283297
timestamp: _dt.datetime
284298
"""
285299
The time the event was triggered."""
@@ -323,6 +337,22 @@ class BeforeSignInResponse(BeforeCreateResponse, total=False):
323337
"""The user's session claims object if available."""
324338

325339

340+
class BeforeEmailSentResponse(_typing.TypedDict, total=False):
341+
"""
342+
The handler response type for 'before_email_sent' blocking events.
343+
"""
344+
345+
recaptcha_action_override: RecaptchaActionOptions | None
346+
347+
348+
class BeforeSmsSentResponse(BeforeEmailSentResponse, total=False):
349+
"""
350+
The handler response type for 'before_sms_sent' blocking events.
351+
"""
352+
353+
recaptcha_action_override: RecaptchaActionOptions | None
354+
355+
326356
BeforeUserCreatedCallable = _typing.Callable[[AuthBlockingEvent],
327357
BeforeCreateResponse | None]
328358
"""
@@ -335,6 +365,18 @@ class BeforeSignInResponse(BeforeCreateResponse, total=False):
335365
The type of the callable for 'before_user_signed_in' blocking events.
336366
"""
337367

368+
BeforeEmailSentCallable = _typing.Callable[[AuthBlockingEvent],
369+
BeforeEmailSentResponse | None]
370+
"""
371+
The type of the callable for 'before_email_sent' blocking events.
372+
"""
373+
374+
BeforeSmsSentCallable = _typing.Callable[[AuthBlockingEvent],
375+
BeforeSmsSentResponse | None]
376+
"""
377+
The type of the callable for 'before_sms_sent' blocking events.
378+
"""
379+
338380

339381
@_util.copy_func_kwargs(_options.BlockingOptions)
340382
def before_user_signed_in(
@@ -442,3 +484,110 @@ def before_user_created_wrapped(request: _Request) -> _Response:
442484
return before_user_created_wrapped
443485

444486
return before_user_created_decorator
487+
488+
@_util.copy_func_kwargs(_options.BlockingOptions)
489+
def before_email_sent(
490+
**kwargs,
491+
) -> _typing.Callable[[BeforeEmailSentCallable], BeforeEmailSentCallable]:
492+
"""
493+
Handles an event that is triggered before a user's email is sent.
494+
495+
Example:
496+
497+
.. code-block:: python
498+
499+
from firebase_functions import identity_fn
500+
501+
@identity_fn.before_email_sent()
502+
def example(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeEmailSentResponse | None:
503+
pass
504+
505+
:param \\*\\*kwargs: Options.
506+
:type \\*\\*kwargs: as :exc:`firebase_functions.options.BlockingOptions`
507+
:rtype: :exc:`typing.Callable`
508+
\\[ \\[ :exc:`firebase_functions.identity_fn.AuthBlockingEvent` \\],
509+
:exc:`firebase_functions.identity_fn.BeforeEmailSentResponse` \\| `None` \\]
510+
A function that takes a AuthBlockingEvent and optionally returns BeforeEmailSentResponse.
511+
"""
512+
options = _options.BlockingOptions(**kwargs)
513+
514+
def before_email_sent_decorator(func: BeforeEmailSentCallable):
515+
from firebase_functions.private._identity_fn import event_type_before_email_sent
516+
517+
@_functools.wraps(func)
518+
def before_email_sent_wrapped(request: _Request) -> _Response:
519+
from firebase_functions.private._identity_fn import before_operation_handler
520+
return before_operation_handler(
521+
func,
522+
event_type_before_email_sent,
523+
request,
524+
)
525+
526+
_util.set_func_endpoint_attr(
527+
before_email_sent_wrapped,
528+
options._endpoint(
529+
func_name=func.__name__,
530+
event_type=event_type_before_email_sent,
531+
),
532+
)
533+
_util.set_required_apis_attr(
534+
before_email_sent_wrapped,
535+
options._required_apis(),
536+
)
537+
return before_email_sent_wrapped
538+
539+
return before_email_sent_decorator
540+
541+
542+
@_util.copy_func_kwargs(_options.BlockingOptions)
543+
def before_sms_sent(
544+
**kwargs,
545+
) -> _typing.Callable[[BeforeSmsSentCallable], BeforeSmsSentCallable]:
546+
"""
547+
Handles an event that is triggered before a user's SMS is sent.
548+
549+
Example:
550+
551+
.. code-block:: python
552+
553+
from firebase_functions import identity_fn
554+
555+
@identity_fn.before_sms_sent()
556+
def example(event: identity_fn.AuthBlockingEvent) -> identity_fn.BeforeSmsSentResponse | None:
557+
pass
558+
559+
:param \\*\\*kwargs: Options.
560+
:type \\*\\*kwargs: as :exc:`firebase_functions.options.BlockingOptions`
561+
:rtype: :exc:`typing.Callable`
562+
\\[ \\[ :exc:`firebase_functions.identity_fn.AuthBlockingEvent` \\],
563+
:exc:`firebase_functions.identity_fn.BeforeSmsSentResponse` \\| `None` \\]
564+
A function that takes a AuthBlockingEvent and optionally returns BeforeSmsSentResponse.
565+
"""
566+
options = _options.BlockingOptions(**kwargs)
567+
568+
def before_sms_sent_decorator(func: BeforeSmsSentCallable):
569+
from firebase_functions.private._identity_fn import event_type_before_sms_sent
570+
571+
@_functools.wraps(func)
572+
def before_sms_sent_wrapped(request: _Request) -> _Response:
573+
from firebase_functions.private._identity_fn import before_operation_handler
574+
return before_operation_handler(
575+
func,
576+
event_type_before_sms_sent,
577+
request,
578+
)
579+
580+
_util.set_func_endpoint_attr(
581+
before_sms_sent_wrapped,
582+
options._endpoint(
583+
func_name=func.__name__,
584+
event_type=event_type_before_sms_sent,
585+
),
586+
)
587+
_util.set_required_apis_attr(
588+
before_sms_sent_wrapped,
589+
options._required_apis(),
590+
)
591+
return before_sms_sent_wrapped
592+
593+
return before_sms_sent_decorator

src/firebase_functions/options.py

+14-7
Original file line numberDiff line numberDiff line change
@@ -994,17 +994,24 @@ def _endpoint(
994994
self,
995995
**kwargs,
996996
) -> _manifest.ManifestEndpoint:
997+
from firebase_functions.private._identity_fn import event_type_before_create, event_type_before_sign_in
998+
997999
assert kwargs["event_type"] is not None
9981000

1001+
blocking_trigger_options: _manifest.BlockingTriggerOptions
1002+
1003+
if kwargs["event_type"] == event_type_before_create or kwargs["event_type"] == event_type_before_sign_in:
1004+
blocking_trigger_options = _manifest.BlockingTriggerOptions(
1005+
idToken=self.id_token if self.id_token is not None else False,
1006+
accessToken=self.access_token if self.access_token is not None else False,
1007+
refreshToken=self.refresh_token if self.refresh_token is not None else False,
1008+
)
1009+
else:
1010+
blocking_trigger_options = _manifest.BlockingTriggerOptions()
1011+
9991012
blocking_trigger = _manifest.BlockingTrigger(
10001013
eventType=kwargs["event_type"],
1001-
options=_manifest.BlockingTriggerOptions(
1002-
idToken=self.id_token if self.id_token is not None else False,
1003-
accessToken=self.access_token
1004-
if self.access_token is not None else False,
1005-
refreshToken=self.refresh_token
1006-
if self.refresh_token is not None else False,
1007-
),
1014+
options=blocking_trigger_options,
10081015
)
10091016

10101017
kwargs_merged = {

src/firebase_functions/private/_identity_fn.py

+17-6
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ def _additional_user_info_from_token_data(token_data: dict[str, _typing.Any]):
167167
username=username,
168168
is_new_user=is_new_user,
169169
recaptcha_score=token_data.get("recaptcha_score"),
170+
email=token_data.get("email"),
171+
phone_number=token_data.get("phone_number"),
170172
)
171173

172174

@@ -200,22 +202,31 @@ def _credential_from_token_data(token_data: dict[str, _typing.Any],
200202
)
201203

202204

203-
def _auth_blocking_event_from_token_data(token_data: dict[str, _typing.Any]):
204-
from firebase_functions.identity_fn import AuthBlockingEvent
205+
def _auth_blocking_event_from_token_data(token_data: dict[str, _typing.Any], event_type: str):
206+
from firebase_functions.identity_fn import AuthBlockingEvent, AuthUserRecord
207+
208+
data: AuthUserRecord | None = None
209+
if event_type == event_type_before_create or event_type == event_type_before_sign_in:
210+
data = _auth_user_record_from_token_data(token_data["user_record"])
211+
205212
return AuthBlockingEvent(
206-
data=_auth_user_record_from_token_data(token_data["user_record"]),
213+
data=data,
207214
locale=token_data.get("locale"),
208215
event_id=token_data["event_id"],
209216
ip_address=token_data["ip_address"],
210217
user_agent=token_data["user_agent"],
211218
timestamp=_dt.datetime.fromtimestamp(token_data["iat"]),
212219
additional_user_info=_additional_user_info_from_token_data(token_data),
213220
credential=_credential_from_token_data(token_data, _time.time()),
221+
email_type=token_data.get("email_type"),
222+
sms_type=token_data.get("sms_type"),
214223
)
215224

216225

217226
event_type_before_create = "providers/cloud.auth/eventTypes/user.beforeCreate"
218227
event_type_before_sign_in = "providers/cloud.auth/eventTypes/user.beforeSignIn"
228+
event_type_before_email_sent = "providers/cloud.auth/eventTypes/user.beforeSendEmail"
229+
event_type_before_sms_sent = "providers/cloud.auth/eventTypes/user.beforeSendSms"
219230

220231

221232
def _validate_auth_response(
@@ -338,7 +349,7 @@ def before_operation_handler(
338349
event_type: str,
339350
request: _Request,
340351
) -> _Response:
341-
from firebase_functions.identity_fn import BeforeCreateResponse, BeforeSignInResponse
352+
from firebase_functions.identity_fn import BeforeCreateResponse, BeforeSignInResponse, BeforeEmailSentResponse, BeforeSmsSentResponse
342353
try:
343354
if not _util.valid_on_call_request(request):
344355
_logging.error("Invalid request, unable to process.")
@@ -351,8 +362,8 @@ def before_operation_handler(
351362
raise HttpsError(FunctionsErrorCode.INVALID_ARGUMENT, "Bad Request")
352363
jwt_token = request.json["data"]["jwt"]
353364
decoded_token = _token_verifier.verify_auth_blocking_token(jwt_token)
354-
event = _auth_blocking_event_from_token_data(decoded_token)
355-
auth_response: BeforeCreateResponse | BeforeSignInResponse | None = _with_init(
365+
event = _auth_blocking_event_from_token_data(decoded_token, event_type)
366+
auth_response: BeforeCreateResponse | BeforeSignInResponse | BeforeEmailSentResponse | BeforeSmsSentResponse | None = _with_init(
356367
func)(event)
357368
if not auth_response:
358369
return _jsonify({})

0 commit comments

Comments
 (0)