Skip to content

Commit e7faccb

Browse files
authored
Merge pull request #44 from invertase/enforce-app-check
feat: support `enforce_app_check` option
2 parents 39926ed + ff42b2f commit e7faccb

File tree

3 files changed

+37
-14
lines changed

3 files changed

+37
-14
lines changed

src/firebase_functions/https_fn.py

+25-12
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import firebase_functions.core as _core
2626
from functions_framework import logging as _logging
2727

28-
from firebase_functions.options import HttpsOptions
28+
from firebase_functions.options import HttpsOptions, _GLOBAL_OPTIONS
2929
from flask import Request, Response, make_response as _make_response, jsonify as _jsonify
3030
from flask_cors import cross_origin as _cross_origin
3131

@@ -346,7 +346,8 @@ class CallableRequest(_typing.Generic[_core.T]):
346346
_C2 = _typing.Callable[[CallableRequest[_typing.Any]], _typing.Any]
347347

348348

349-
def _on_call_handler(func: _C2, request: Request) -> Response:
349+
def _on_call_handler(func: _C2, request: Request,
350+
enforce_app_check: bool) -> Response:
350351
try:
351352
if not _util.valid_on_call_request(request):
352353
_logging.error("Invalid request, unable to process.")
@@ -362,23 +363,22 @@ def _on_call_handler(func: _C2, request: Request) -> Response:
362363
raise HttpsError(FunctionsErrorCode.UNAUTHENTICATED,
363364
"Unauthenticated")
364365

365-
# TODO support for `allowInvalidAppCheckToken`
366-
if token_status.app == _util.OnCallTokenState.INVALID:
366+
if enforce_app_check and token_status.app in (
367+
_util.OnCallTokenState.MISSING, _util.OnCallTokenState.INVALID):
367368
raise HttpsError(FunctionsErrorCode.UNAUTHENTICATED,
368369
"Unauthenticated")
369-
370-
if token_status.auth_token is not None:
370+
if token_status.app == _util.OnCallTokenState.VALID and token_status.app_token is not None:
371371
context = _dataclasses.replace(
372372
context,
373-
auth=AuthData(token_status.auth_token["uid"],
374-
token_status.auth_token),
373+
app=AppCheckData(token_status.app_token["sub"],
374+
token_status.app_token),
375375
)
376376

377-
if token_status.app_token is not None:
377+
if token_status.auth_token is not None:
378378
context = _dataclasses.replace(
379379
context,
380-
app=AppCheckData(token_status.app_token["sub"],
381-
token_status.app_token),
380+
auth=AuthData(token_status.auth_token["uid"],
381+
token_status.auth_token),
382382
)
383383

384384
instance_id = request.headers.get("Firebase-Instance-ID-Token")
@@ -474,13 +474,26 @@ def on_call_inner_decorator(func: _C2):
474474
if options.cors is not None and options.cors.cors_origins is not None:
475475
origins = options.cors.cors_origins
476476

477+
# Default to False.
478+
enforce_app_check = False
479+
# If the global option is set, use that.
480+
if options.enforce_app_check is None and _GLOBAL_OPTIONS.enforce_app_check is not None:
481+
enforce_app_check = _GLOBAL_OPTIONS.enforce_app_check
482+
# If the global option is not set, use the local option.
483+
elif options.enforce_app_check is not None:
484+
enforce_app_check = options.enforce_app_check
485+
477486
@_cross_origin(
478487
methods="POST",
479488
origins=origins,
480489
)
481490
@_functools.wraps(func)
482491
def on_call_wrapped(request: Request):
483-
return _on_call_handler(func, request)
492+
return _on_call_handler(
493+
func,
494+
request,
495+
enforce_app_check,
496+
)
484497

485498
_util.set_func_endpoint_attr(
486499
on_call_wrapped,

src/firebase_functions/options.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,14 @@ class RuntimeOptions:
196196
Secrets to bind to a function.
197197
"""
198198

199+
enforce_app_check: bool | None = None
200+
"""
201+
Determines whether Firebase AppCheck is enforced.
202+
When true, requests with invalid tokens auto respond with a 401
203+
Unauthorized response.
204+
When false, requests with invalid tokens set event.app to None.
205+
"""
206+
199207
def _asdict_with_global_options(self) -> dict:
200208
"""
201209
Returns the provider options merged with globally defined options.
@@ -438,7 +446,7 @@ def _asdict_with_global_options(self) -> dict:
438446
"""
439447
merged_options = super()._asdict_with_global_options()
440448
# "cors" is only used locally by the functions framework
441-
# and is not used in the manifest.
449+
# and is not used in the manifest or in global options.
442450
if "cors" in merged_options:
443451
del merged_options["cors"]
444452
return merged_options
@@ -499,6 +507,7 @@ def set_global_options(
499507
ingress: IngressSetting | _util.Sentinel | None = None,
500508
labels: dict[str, str] | None = None,
501509
secrets: list[str] | list[SecretParam] | _util.Sentinel | None = None,
510+
enforce_app_check: bool | None = None,
502511
):
503512
"""
504513
Sets default options for all functions.
@@ -518,4 +527,5 @@ def set_global_options(
518527
ingress=ingress,
519528
labels=labels,
520529
secrets=secrets,
530+
enforce_app_check=enforce_app_check,
521531
)

src/firebase_functions/private/util.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ class _OnCallTokenVerification:
155155
"""
156156

157157
app: OnCallTokenState = OnCallTokenState.INVALID
158-
app_token: _typing.Optional[dict] = None
158+
app_token: _typing.Optional[dict[str, _typing.Any]] = None
159159
auth: OnCallTokenState = OnCallTokenState.INVALID
160160
auth_token: _typing.Optional[dict] = None
161161

0 commit comments

Comments
 (0)