From 29523657e4e5839c63a5d00fa2ef5a1c080a1836 Mon Sep 17 00:00:00 2001 From: exaby73 Date: Fri, 2 Feb 2024 06:28:29 +0530 Subject: [PATCH 01/17] fix: Remove auth check for cloud tasks --- src/firebase_functions/https_fn.py | 9 ++++--- src/firebase_functions/private/util.py | 37 +++++++++++++++++++++++--- tests/test_util.py | 9 ++++++- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/src/firebase_functions/https_fn.py b/src/firebase_functions/https_fn.py index 84c5659..6817ec9 100644 --- a/src/firebase_functions/https_fn.py +++ b/src/firebase_functions/https_fn.py @@ -346,8 +346,10 @@ class CallableRequest(_typing.Generic[_core.T]): _C2 = _typing.Callable[[CallableRequest[_typing.Any]], _typing.Any] -def _on_call_handler(func: _C2, request: Request, - enforce_app_check: bool) -> Response: +def _on_call_handler(func: _C2, + request: Request, + enforce_app_check: bool, + verify_token: bool = True) -> Response: try: if not _util.valid_on_call_request(request): _logging.error("Invalid request, unable to process.") @@ -357,7 +359,8 @@ def _on_call_handler(func: _C2, request: Request, data=_json.loads(request.data)["data"], ) - token_status = _util.on_call_check_tokens(request) + token_status = _util.on_call_check_tokens(request, + verify_token=verify_token) if token_status.auth == _util.OnCallTokenState.INVALID: raise HttpsError(FunctionsErrorCode.UNAUTHENTICATED, diff --git a/src/firebase_functions/private/util.py b/src/firebase_functions/private/util.py index 0997f8d..a0c1028 100644 --- a/src/firebase_functions/private/util.py +++ b/src/firebase_functions/private/util.py @@ -15,8 +15,10 @@ Module for internal utilities. """ +import base64 import os as _os import json as _json +import re as _re import typing as _typing import dataclasses as _dataclasses import datetime as _dt @@ -29,6 +31,9 @@ P = _typing.ParamSpec("P") R = _typing.TypeVar("R") +JWT_REGEX = _re.compile( + r'^[a-zA-Z0-9\-_=]+?\.[a-zA-Z0-9\-_=]+?\.([a-zA-Z0-9\-_=]+)?$') + class Sentinel: """Internal class for RESET_VALUE.""" @@ -204,7 +209,8 @@ def as_dict(self) -> dict: def _on_call_check_auth_token( - request: _Request + request: _Request, + verify_token: bool = True, ) -> None | _typing.Literal[OnCallTokenState.INVALID] | dict[str, _typing.Any]: """Validates the auth token in a callable request.""" authorization = request.headers.get("Authorization") @@ -215,7 +221,10 @@ def _on_call_check_auth_token( return OnCallTokenState.INVALID try: id_token = authorization.replace("Bearer ", "") - auth_token = _auth.verify_id_token(id_token) + if verify_token: + auth_token = _auth.verify_id_token(id_token) + else: + auth_token = _unsafe_decode_id_token(id_token) return auth_token # pylint: disable=broad-except except Exception as err: @@ -240,11 +249,31 @@ def _on_call_check_app_token( return OnCallTokenState.INVALID -def on_call_check_tokens(request: _Request,) -> _OnCallTokenVerification: +def _unsafe_decode_id_token(token: str): + # Check if the token matches the JWT pattern + if not JWT_REGEX.match(token): + return {} + + # Split the token by '.' and decode each component from base64 + components = [base64.urlsafe_b64decode(f"{s}==") for s in token.split('.')] + + # Attempt to parse the payload (second component) as JSON + payload = components[1].decode('utf-8') + try: + payload = _json.loads(payload) + except _json.JSONDecodeError: + # If there's an error during parsing, ignore it and return the payload as is + pass + + return payload + + +def on_call_check_tokens(request: _Request, + verify_token: bool = True) -> _OnCallTokenVerification: """Check tokens""" verifications = _OnCallTokenVerification() - auth_token = _on_call_check_auth_token(request) + auth_token = _on_call_check_auth_token(request, verify_token=verify_token) if auth_token is None: verifications.auth = OnCallTokenState.MISSING elif isinstance(auth_token, dict): diff --git a/tests/test_util.py b/tests/test_util.py index 0a8dcbb..efc7c6c 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -15,7 +15,7 @@ Internal utils tests. """ from os import environ, path -from firebase_functions.private.util import firebase_config, microsecond_timestamp_conversion, nanoseconds_timestamp_conversion, get_precision_timestamp, normalize_path, deep_merge, PrecisionTimestamp, second_timestamp_conversion +from firebase_functions.private.util import firebase_config, microsecond_timestamp_conversion, nanoseconds_timestamp_conversion, get_precision_timestamp, normalize_path, deep_merge, PrecisionTimestamp, second_timestamp_conversion, _unsafe_decode_id_token import datetime as _dt test_bucket = "python-functions-testing.appspot.com" @@ -184,3 +184,10 @@ def test_does_not_modify_originals(): deep_merge(dict1, dict2) assert dict1["baz"]["answer"] == 42 assert dict2["baz"]["answer"] == 33 + + +def test_unsafe_decode_token(): + TEST_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmaXJlYmFzZSIsIm5hbWUiOiJKb2huIERvZSJ9.74A24Y821E7CZx8aYCsCKo0Y-W0qXwqME-14QlEMcB0" + result = _unsafe_decode_id_token(TEST_TOKEN) + assert result['sub'] == "firebase" + assert result['name'] == "John Doe" From 1b7fe5327d1f49b1d63e58684c99ad9b18040a6a Mon Sep 17 00:00:00 2001 From: exaby73 Date: Fri, 2 Feb 2024 08:26:07 +0530 Subject: [PATCH 02/17] skip app token check as well --- src/firebase_functions/private/util.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/firebase_functions/private/util.py b/src/firebase_functions/private/util.py index a0c1028..00b07d5 100644 --- a/src/firebase_functions/private/util.py +++ b/src/firebase_functions/private/util.py @@ -161,7 +161,8 @@ def _on_call_valid_content_type(request: _Request) -> bool: # Check that the Content-Type is JSON. if content_type.lower() != "application/json": - _logging.warning("Request has incorrect Content-Type: %s", content_type) + _logging.warning( + "Request has incorrect Content-Type: %s", content_type) return False return True @@ -234,14 +235,18 @@ def _on_call_check_auth_token( def _on_call_check_app_token( - request: _Request + request: _Request, + verify_token: bool = True, ) -> None | _typing.Literal[OnCallTokenState.INVALID] | dict[str, _typing.Any]: """Validates the app token in a callable request.""" app_check = request.headers.get("X-Firebase-AppCheck") if app_check is None: return None try: - app_token = _app_check.verify_token(app_check) + if verify_token: + app_token = _app_check.verify_token(app_check) + else: + app_token = _unsafe_decode_id_token(app_check) return app_token # pylint: disable=broad-except except Exception as err: @@ -280,7 +285,7 @@ def on_call_check_tokens(request: _Request, verifications.auth = OnCallTokenState.VALID verifications.auth_token = auth_token - app_token = _on_call_check_app_token(request) + app_token = _on_call_check_app_token(request, verify_token=verify_token) if app_token is None: verifications.app = OnCallTokenState.MISSING elif isinstance(app_token, dict): From 730dcb90ad0b54cbfceddd5e124135fb1bb4f801 Mon Sep 17 00:00:00 2001 From: exaby73 Date: Fri, 2 Feb 2024 08:35:50 +0530 Subject: [PATCH 03/17] Add debug logging for auth_token and app_token --- src/firebase_functions/private/util.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/firebase_functions/private/util.py b/src/firebase_functions/private/util.py index 00b07d5..ecf74bb 100644 --- a/src/firebase_functions/private/util.py +++ b/src/firebase_functions/private/util.py @@ -28,6 +28,8 @@ from firebase_admin import auth as _auth from firebase_admin import app_check as _app_check +import logger + P = _typing.ParamSpec("P") R = _typing.TypeVar("R") @@ -279,6 +281,7 @@ def on_call_check_tokens(request: _Request, verifications = _OnCallTokenVerification() auth_token = _on_call_check_auth_token(request, verify_token=verify_token) + logger.debug(f"auth_token: {auth_token}") if auth_token is None: verifications.auth = OnCallTokenState.MISSING elif isinstance(auth_token, dict): @@ -286,6 +289,7 @@ def on_call_check_tokens(request: _Request, verifications.auth_token = auth_token app_token = _on_call_check_app_token(request, verify_token=verify_token) + logger.debug(f"app_token: {app_token}") if app_token is None: verifications.app = OnCallTokenState.MISSING elif isinstance(app_token, dict): From 3b7f1a00cccc4d452e25c9ba1598c3c6c443e900 Mon Sep 17 00:00:00 2001 From: exaby73 Date: Fri, 2 Feb 2024 08:52:52 +0530 Subject: [PATCH 04/17] Update logger statements in util.py --- src/firebase_functions/private/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/firebase_functions/private/util.py b/src/firebase_functions/private/util.py index ecf74bb..4197f71 100644 --- a/src/firebase_functions/private/util.py +++ b/src/firebase_functions/private/util.py @@ -281,7 +281,7 @@ def on_call_check_tokens(request: _Request, verifications = _OnCallTokenVerification() auth_token = _on_call_check_auth_token(request, verify_token=verify_token) - logger.debug(f"auth_token: {auth_token}") + logger.warn(f"auth_token: {auth_token}") if auth_token is None: verifications.auth = OnCallTokenState.MISSING elif isinstance(auth_token, dict): @@ -289,7 +289,7 @@ def on_call_check_tokens(request: _Request, verifications.auth_token = auth_token app_token = _on_call_check_app_token(request, verify_token=verify_token) - logger.debug(f"app_token: {app_token}") + logger.warn(f"app_token: {app_token}") if app_token is None: verifications.app = OnCallTokenState.MISSING elif isinstance(app_token, dict): From be795537d57d4d721d292f30c45cc8b9ac2f185f Mon Sep 17 00:00:00 2001 From: exaby73 Date: Fri, 2 Feb 2024 09:11:51 +0530 Subject: [PATCH 05/17] Refactor logger import in util.py --- src/firebase_functions/private/util.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/firebase_functions/private/util.py b/src/firebase_functions/private/util.py index 4197f71..ad0b08c 100644 --- a/src/firebase_functions/private/util.py +++ b/src/firebase_functions/private/util.py @@ -27,8 +27,7 @@ from functions_framework import logging as _logging from firebase_admin import auth as _auth from firebase_admin import app_check as _app_check - -import logger +from firebase_functions import logger P = _typing.ParamSpec("P") R = _typing.TypeVar("R") From 6baa839a315a6e13259f6ccd4448f7decf318d92 Mon Sep 17 00:00:00 2001 From: exaby73 Date: Fri, 2 Feb 2024 09:25:40 +0530 Subject: [PATCH 06/17] Add debug logs for Authorization and X-Firebase-AppCheck headers --- src/firebase_functions/private/util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/firebase_functions/private/util.py b/src/firebase_functions/private/util.py index ad0b08c..0d42002 100644 --- a/src/firebase_functions/private/util.py +++ b/src/firebase_functions/private/util.py @@ -216,6 +216,7 @@ def _on_call_check_auth_token( ) -> None | _typing.Literal[OnCallTokenState.INVALID] | dict[str, _typing.Any]: """Validates the auth token in a callable request.""" authorization = request.headers.get("Authorization") + logger.debug(f"Authorization (verify_token={verify_token}): {authorization}") if authorization is None: return None if not authorization.startswith("Bearer "): @@ -241,6 +242,7 @@ def _on_call_check_app_token( ) -> None | _typing.Literal[OnCallTokenState.INVALID] | dict[str, _typing.Any]: """Validates the app token in a callable request.""" app_check = request.headers.get("X-Firebase-AppCheck") + logger.debug(f"X-Firebase-AppCheck (verify_token={verify_token}): {app_check}") if app_check is None: return None try: From 999f0c97401152e862624e6b19c0ba28da13b4ba Mon Sep 17 00:00:00 2001 From: exaby73 Date: Fri, 2 Feb 2024 09:31:13 +0530 Subject: [PATCH 07/17] Refactor debug log statements in util.py --- src/firebase_functions/private/util.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/firebase_functions/private/util.py b/src/firebase_functions/private/util.py index 0d42002..edd29cb 100644 --- a/src/firebase_functions/private/util.py +++ b/src/firebase_functions/private/util.py @@ -216,7 +216,8 @@ def _on_call_check_auth_token( ) -> None | _typing.Literal[OnCallTokenState.INVALID] | dict[str, _typing.Any]: """Validates the auth token in a callable request.""" authorization = request.headers.get("Authorization") - logger.debug(f"Authorization (verify_token={verify_token}): {authorization}") + logger.debug( + f"Authorization (verify_token={verify_token}): {authorization}") if authorization is None: return None if not authorization.startswith("Bearer "): @@ -242,7 +243,10 @@ def _on_call_check_app_token( ) -> None | _typing.Literal[OnCallTokenState.INVALID] | dict[str, _typing.Any]: """Validates the app token in a callable request.""" app_check = request.headers.get("X-Firebase-AppCheck") - logger.debug(f"X-Firebase-AppCheck (verify_token={verify_token}): {app_check}") + logger.debug( + f"X-Firebase-AppCheck (verify_token={verify_token}): {app_check}") + logger.debug("All headers", headers={ + k: v for k, v in request.headers.items()}) if app_check is None: return None try: From 2fed3a63b08671e55468cbd75d943c614b7b013d Mon Sep 17 00:00:00 2001 From: exaby73 Date: Fri, 2 Feb 2024 09:51:39 +0530 Subject: [PATCH 08/17] Refactor token verification logic in util.py and tasks_fn.py --- src/firebase_functions/private/util.py | 33 +++++++++----------------- src/firebase_functions/tasks_fn.py | 2 +- 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/src/firebase_functions/private/util.py b/src/firebase_functions/private/util.py index edd29cb..7b14ed1 100644 --- a/src/firebase_functions/private/util.py +++ b/src/firebase_functions/private/util.py @@ -214,10 +214,8 @@ def _on_call_check_auth_token( request: _Request, verify_token: bool = True, ) -> None | _typing.Literal[OnCallTokenState.INVALID] | dict[str, _typing.Any]: - """Validates the auth token in a callable request.""" + """Validates the auth token in a callable request. If verify_token is False, the token will be decoded without verification.""" authorization = request.headers.get("Authorization") - logger.debug( - f"Authorization (verify_token={verify_token}): {authorization}") if authorization is None: return None if not authorization.startswith("Bearer "): @@ -234,26 +232,17 @@ def _on_call_check_auth_token( except Exception as err: _logging.error(f"Error validating token: {err}") return OnCallTokenState.INVALID - return OnCallTokenState.INVALID def _on_call_check_app_token( - request: _Request, - verify_token: bool = True, + request: _Request ) -> None | _typing.Literal[OnCallTokenState.INVALID] | dict[str, _typing.Any]: """Validates the app token in a callable request.""" app_check = request.headers.get("X-Firebase-AppCheck") - logger.debug( - f"X-Firebase-AppCheck (verify_token={verify_token}): {app_check}") - logger.debug("All headers", headers={ - k: v for k, v in request.headers.items()}) if app_check is None: return None try: - if verify_token: - app_token = _app_check.verify_token(app_check) - else: - app_token = _unsafe_decode_id_token(app_check) + app_token = _app_check.verify_token(app_check) return app_token # pylint: disable=broad-except except Exception as err: @@ -293,13 +282,13 @@ def on_call_check_tokens(request: _Request, verifications.auth = OnCallTokenState.VALID verifications.auth_token = auth_token - app_token = _on_call_check_app_token(request, verify_token=verify_token) - logger.warn(f"app_token: {app_token}") - if app_token is None: - verifications.app = OnCallTokenState.MISSING - elif isinstance(app_token, dict): - verifications.app = OnCallTokenState.VALID - verifications.app_token = app_token + if verify_token: + app_token = _on_call_check_app_token(request) + if app_token is None: + verifications.app = OnCallTokenState.MISSING + elif isinstance(app_token, dict): + verifications.app = OnCallTokenState.VALID + verifications.app_token = app_token log_payload = { **verifications.as_dict(), @@ -309,7 +298,7 @@ def on_call_check_tokens(request: _Request, } errs = [] - if verifications.app == OnCallTokenState.INVALID: + if verify_token and verifications.app == OnCallTokenState.INVALID: errs.append(("AppCheck token was rejected.", log_payload)) if verifications.auth == OnCallTokenState.INVALID: diff --git a/src/firebase_functions/tasks_fn.py b/src/firebase_functions/tasks_fn.py index 2d366b6..19947fa 100644 --- a/src/firebase_functions/tasks_fn.py +++ b/src/firebase_functions/tasks_fn.py @@ -53,7 +53,7 @@ def on_task_dispatched_decorator(func: _C): @_functools.wraps(func) def on_task_dispatched_wrapped(request: Request) -> Response: - return _on_call_handler(func, request, enforce_app_check=False) + return _on_call_handler(func, request, verify_token=False) _util.set_func_endpoint_attr( on_task_dispatched_wrapped, From a9893913e42962bedc763638d8b56450bfb27386 Mon Sep 17 00:00:00 2001 From: exaby73 Date: Fri, 2 Feb 2024 09:56:17 +0530 Subject: [PATCH 09/17] Revert on_task_dispatched function to include enforce_app_check parameter --- src/firebase_functions/tasks_fn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/firebase_functions/tasks_fn.py b/src/firebase_functions/tasks_fn.py index 19947fa..ad0584a 100644 --- a/src/firebase_functions/tasks_fn.py +++ b/src/firebase_functions/tasks_fn.py @@ -53,7 +53,7 @@ def on_task_dispatched_decorator(func: _C): @_functools.wraps(func) def on_task_dispatched_wrapped(request: Request) -> Response: - return _on_call_handler(func, request, verify_token=False) + return _on_call_handler(func, request, enforce_app_check=False, verify_token=False) _util.set_func_endpoint_attr( on_task_dispatched_wrapped, From a7da435587a0b778459d4ee908c19259a89c5138 Mon Sep 17 00:00:00 2001 From: exaby73 Date: Fri, 2 Feb 2024 22:22:52 +0530 Subject: [PATCH 10/17] Fix error logging in _on_call_handler function --- src/firebase_functions/https_fn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/firebase_functions/https_fn.py b/src/firebase_functions/https_fn.py index 6817ec9..7bb1dbc 100644 --- a/src/firebase_functions/https_fn.py +++ b/src/firebase_functions/https_fn.py @@ -402,7 +402,7 @@ def _on_call_handler(func: _C2, # pylint: disable=broad-except except Exception as err: if not isinstance(err, HttpsError): - _logging.error("Unhandled error", err) + _logging.error("Unhandled error: %s", err) err = HttpsError(FunctionsErrorCode.INTERNAL, "INTERNAL") status = err._http_error_code.status return _make_response(_jsonify(error=err._as_dict()), status) From 24b777de5f25f20fd52fafade3544832ce9eb443 Mon Sep 17 00:00:00 2001 From: exaby73 Date: Sat, 3 Feb 2024 05:28:14 +0530 Subject: [PATCH 11/17] Add debug logging for auth token --- src/firebase_functions/https_fn.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/firebase_functions/https_fn.py b/src/firebase_functions/https_fn.py index 7bb1dbc..53769fd 100644 --- a/src/firebase_functions/https_fn.py +++ b/src/firebase_functions/https_fn.py @@ -378,6 +378,7 @@ def _on_call_handler(func: _C2, ) if token_status.auth_token is not None: + _logging.debug(token_status.auth_token) context = _dataclasses.replace( context, auth=AuthData(token_status.auth_token["uid"], From 3ef480e10afd8543411e5ec710c3e383758cb8a4 Mon Sep 17 00:00:00 2001 From: exaby73 Date: Sat, 3 Feb 2024 05:32:11 +0530 Subject: [PATCH 12/17] Add check for uid in auth_token --- src/firebase_functions/https_fn.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/firebase_functions/https_fn.py b/src/firebase_functions/https_fn.py index 53769fd..b855b7a 100644 --- a/src/firebase_functions/https_fn.py +++ b/src/firebase_functions/https_fn.py @@ -377,8 +377,7 @@ def _on_call_handler(func: _C2, token_status.app_token), ) - if token_status.auth_token is not None: - _logging.debug(token_status.auth_token) + if token_status.auth_token is not None and "uid" in token_status.auth_token: context = _dataclasses.replace( context, auth=AuthData(token_status.auth_token["uid"], From ec14738c1d3331f1cfc80f75768bc7a4b9681408 Mon Sep 17 00:00:00 2001 From: exaby73 Date: Sat, 3 Feb 2024 05:37:22 +0530 Subject: [PATCH 13/17] Remove unused logger import and log statement --- src/firebase_functions/private/util.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/firebase_functions/private/util.py b/src/firebase_functions/private/util.py index 7b14ed1..9064d3d 100644 --- a/src/firebase_functions/private/util.py +++ b/src/firebase_functions/private/util.py @@ -27,7 +27,6 @@ from functions_framework import logging as _logging from firebase_admin import auth as _auth from firebase_admin import app_check as _app_check -from firebase_functions import logger P = _typing.ParamSpec("P") R = _typing.TypeVar("R") @@ -275,7 +274,6 @@ def on_call_check_tokens(request: _Request, verifications = _OnCallTokenVerification() auth_token = _on_call_check_auth_token(request, verify_token=verify_token) - logger.warn(f"auth_token: {auth_token}") if auth_token is None: verifications.auth = OnCallTokenState.MISSING elif isinstance(auth_token, dict): From 977c5170c69de23204e4e4b18db4eac2768c997c Mon Sep 17 00:00:00 2001 From: exaby73 Date: Sat, 3 Feb 2024 05:42:18 +0530 Subject: [PATCH 14/17] Lint fixes --- src/firebase_functions/private/util.py | 11 +++++++---- src/firebase_functions/tasks_fn.py | 5 ++++- tests/test_util.py | 9 +++++---- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/firebase_functions/private/util.py b/src/firebase_functions/private/util.py index 9064d3d..dc0fa75 100644 --- a/src/firebase_functions/private/util.py +++ b/src/firebase_functions/private/util.py @@ -32,7 +32,7 @@ R = _typing.TypeVar("R") JWT_REGEX = _re.compile( - r'^[a-zA-Z0-9\-_=]+?\.[a-zA-Z0-9\-_=]+?\.([a-zA-Z0-9\-_=]+)?$') + r"^[a-zA-Z0-9\-_=]+?\.[a-zA-Z0-9\-_=]+?\.([a-zA-Z0-9\-_=]+)?$") class Sentinel: @@ -213,7 +213,10 @@ def _on_call_check_auth_token( request: _Request, verify_token: bool = True, ) -> None | _typing.Literal[OnCallTokenState.INVALID] | dict[str, _typing.Any]: - """Validates the auth token in a callable request. If verify_token is False, the token will be decoded without verification.""" + """ + Validates the auth token in a callable request. + If verify_token is False, the token will be decoded without verification. + """ authorization = request.headers.get("Authorization") if authorization is None: return None @@ -255,10 +258,10 @@ def _unsafe_decode_id_token(token: str): return {} # Split the token by '.' and decode each component from base64 - components = [base64.urlsafe_b64decode(f"{s}==") for s in token.split('.')] + components = [base64.urlsafe_b64decode(f"{s}==") for s in token.split(".")] # Attempt to parse the payload (second component) as JSON - payload = components[1].decode('utf-8') + payload = components[1].decode("utf-8") try: payload = _json.loads(payload) except _json.JSONDecodeError: diff --git a/src/firebase_functions/tasks_fn.py b/src/firebase_functions/tasks_fn.py index ad0584a..e0ecf3b 100644 --- a/src/firebase_functions/tasks_fn.py +++ b/src/firebase_functions/tasks_fn.py @@ -53,7 +53,10 @@ def on_task_dispatched_decorator(func: _C): @_functools.wraps(func) def on_task_dispatched_wrapped(request: Request) -> Response: - return _on_call_handler(func, request, enforce_app_check=False, verify_token=False) + return _on_call_handler(func, + request, + enforce_app_check=False, + verify_token=False) _util.set_func_endpoint_attr( on_task_dispatched_wrapped, diff --git a/tests/test_util.py b/tests/test_util.py index efc7c6c..cb13d30 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -187,7 +187,8 @@ def test_does_not_modify_originals(): def test_unsafe_decode_token(): - TEST_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmaXJlYmFzZSIsIm5hbWUiOiJKb2huIERvZSJ9.74A24Y821E7CZx8aYCsCKo0Y-W0qXwqME-14QlEMcB0" - result = _unsafe_decode_id_token(TEST_TOKEN) - assert result['sub'] == "firebase" - assert result['name'] == "John Doe" + # pylint: disable=line-too-long + test_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmaXJlYmFzZSIsIm5hbWUiOiJKb2huIERvZSJ9.74A24Y821E7CZx8aYCsCKo0Y-W0qXwqME-14QlEMcB0" + result = _unsafe_decode_id_token(test_token) + assert result["sub"] == "firebase" + assert result["name"] == "John Doe" From 588a60ddd8246e82768876981ba5318e4e091d74 Mon Sep 17 00:00:00 2001 From: exaby73 Date: Sat, 3 Feb 2024 05:45:44 +0530 Subject: [PATCH 15/17] Formatting fix --- src/firebase_functions/private/util.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/firebase_functions/private/util.py b/src/firebase_functions/private/util.py index dc0fa75..4a939af 100644 --- a/src/firebase_functions/private/util.py +++ b/src/firebase_functions/private/util.py @@ -161,8 +161,7 @@ def _on_call_valid_content_type(request: _Request) -> bool: # Check that the Content-Type is JSON. if content_type.lower() != "application/json": - _logging.warning( - "Request has incorrect Content-Type: %s", content_type) + _logging.warning("Request has incorrect Content-Type: %s", content_type) return False return True From 0cacaa9430498d410607a6e81464322b4c6b9d9f Mon Sep 17 00:00:00 2001 From: exaby73 Date: Wed, 7 Feb 2024 17:59:18 +0530 Subject: [PATCH 16/17] Update AuthData class to allow None for uid Add test case to verify token decoding --- src/firebase_functions/https_fn.py | 10 +++++---- tests/test_tasks_fn.py | 34 ++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/firebase_functions/https_fn.py b/src/firebase_functions/https_fn.py index b855b7a..45cdc5e 100644 --- a/src/firebase_functions/https_fn.py +++ b/src/firebase_functions/https_fn.py @@ -280,7 +280,7 @@ class AuthData: The interface for Auth tokens verified in Callable functions """ - uid: str + uid: str | None """ User ID of the ID token. """ @@ -377,11 +377,13 @@ def _on_call_handler(func: _C2, token_status.app_token), ) - if token_status.auth_token is not None and "uid" in token_status.auth_token: + if token_status.auth_token is not None: context = _dataclasses.replace( context, - auth=AuthData(token_status.auth_token["uid"], - token_status.auth_token), + auth=AuthData( + token_status.auth_token["uid"] + if "uid" in token_status.auth_token else None, + token_status.auth_token), ) instance_id = request.headers.get("Firebase-Instance-ID-Token") diff --git a/tests/test_tasks_fn.py b/tests/test_tasks_fn.py index 0e1293a..486955b 100644 --- a/tests/test_tasks_fn.py +++ b/tests/test_tasks_fn.py @@ -68,3 +68,37 @@ def example(request: CallableRequest[object]) -> str: response.get_data(as_text=True), '{"result":"Hello World"}\n', ) + + def test_token_is_decoded(self): + """ + Test that the token is decoded instead of verifying auth first. + """ + app = Flask(__name__) + + @on_task_dispatched() + def example(request: CallableRequest[object]) -> str: + auth = request.auth + # Make mypy happy + if auth is None: + return "No Auth" + self.assertEqual(auth.token["sub"], "firebase") + self.assertEqual(auth.token["name"], "John Doe") + return "Hello World" + + with app.test_request_context("/"): + # pylint: disable=line-too-long + test_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmaXJlYmFzZSIsIm5hbWUiOiJKb2huIERvZSJ9.74A24Y821E7CZx8aYCsCKo0Y-W0qXwqME-14QlEMcB0" + environ = EnvironBuilder( + method="POST", + headers={ + "Authorization": f"Bearer {test_token}" + }, + json={ + "data": { + "test": "value" + }, + }, + ).get_environ() + request = Request(environ) + response = example(request) + self.assertEqual(response.status_code, 200) From 390d2a88254c709e0b7296025d7519545b875a78 Mon Sep 17 00:00:00 2001 From: exaby73 Date: Thu, 8 Feb 2024 15:10:08 +0530 Subject: [PATCH 17/17] Add check for None value in auth variable --- tests/test_tasks_fn.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_tasks_fn.py b/tests/test_tasks_fn.py index 486955b..b52bd0f 100644 --- a/tests/test_tasks_fn.py +++ b/tests/test_tasks_fn.py @@ -80,6 +80,7 @@ def example(request: CallableRequest[object]) -> str: auth = request.auth # Make mypy happy if auth is None: + self.fail("Auth is None") return "No Auth" self.assertEqual(auth.token["sub"], "firebase") self.assertEqual(auth.token["name"], "John Doe")