Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b17f8f1
add initial pipelines snippet file
morganchen12 Nov 3, 2025
71c481f
add pipeline snippets for python
morganchen12 Nov 4, 2025
cf8b63e
address feedback except formatting
morganchen12 Nov 6, 2025
45302f8
run format
morganchen12 Nov 6, 2025
9df189b
remove confusing async bit
morganchen12 Nov 7, 2025
f84c39d
Add expression and input snippets, with errors
morganchen12 Nov 11, 2025
b17342b
add query explain snippet and fix lint errors
morganchen12 Nov 11, 2025
79e3d2b
fix explainoptions snippet
morganchen12 Nov 12, 2025
addbcbf
add type and constant functions
morganchen12 Jan 14, 2026
84d9c85
Merge branch 'main' into mc/pipelines-snippets
morganchen12 Apr 15, 2026
bff59b7
add snippets except forceindex (not supported)
morganchen12 Apr 15, 2026
c2db983
Add snippets for inserting data used by other snippets
morganchen12 Apr 17, 2026
eb62188
add search and geo snippets
morganchen12 Apr 20, 2026
3ab8ef2
code review feedback
morganchen12 May 15, 2026
7851ad5
Merge branch 'main' into mc/pipelines-snippets
morganchen12 Jun 26, 2026
8ca4f37
feat(fcm): Enable `fid` and deprecate `token` for Send API (#951)
yvonnep165 Jun 29, 2026
a3f6df8
fix(deps): Added universe_domain override to MockGoogleComputeEngineC…
lahirumaramba Jun 29, 2026
ef49e31
chore(deps): update pyjwt requirement from >=2.10.1 to >=2.12.1 (#948)
dependabot[bot] Jun 29, 2026
f3e8ed8
chore(deps): update google-cloud-firestore requirement (#947)
dependabot[bot] Jun 29, 2026
486597b
chore(deps): update pytest requirement from >=8.2.2 to >=8.4.2 (#946)
dependabot[bot] Jun 29, 2026
43e5543
Merge branch 'main' into mc/pipelines-snippets
morganchen12 Jun 29, 2026
4a6ce82
Merge branch 'main' into mc/pipelines-snippets
morganchen12 Jul 31, 2026
c4ba7c0
add snippets for joins and explain (#973)
morganchen12 Jul 31, 2026
b514616
update GitHub Actions to Node 24 versions (#974)
yvonnep165 Aug 13, 2026
1d1df68
chore(ml): Skip flaky ML tests blocking nightly builds (#979)
jonathanedey Sep 1, 2026
4290653
feat(ml): deprecate Machine Learning APIs (#981)
yvonnep165 Sep 1, 2026
f81e4ea
feat(appcheck): Verify one-time tokens for replay protection (#976)
yvonnep165 Sep 16, 2026
197e06d
[chore] Release 7.6.0 (#985)
yvonnep165 Sep 17, 2026
439d011
feat(fcm): Migrate topic management to FCM v1 API (#980)
lahirumaramba Sep 23, 2026
add80a5
fix(appcheck): Use v1beta endpoint for verifyAppCheckToken (#987)
lahirumaramba Sep 23, 2026
d8cef17
[chore] Release 7.7.0 (#988)
lahirumaramba Sep 23, 2026
e7081bb
fix(auth): validate tenant and provider IDs and URL-encode query para…
macastelaz Oct 21, 2026
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
18 changes: 9 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ jobs:
python: ['3.9', '3.10', '3.11', '3.12', '3.13', 'pypy3.9']

steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up Python 3.13 for emulator
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.13'
- name: Setup functions emulator environment
Expand All @@ -29,7 +29,7 @@ jobs:
pip install -r integration/emulators/functions/requirements.txt
deactivate
- name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python }}
- name: Install dependencies
Expand All @@ -38,12 +38,12 @@ jobs:
pip install -r requirements.txt
- name: Test with pytest
run: pytest
- name: Set up Node.js 20
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- name: Set up Node.js 24
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 20
node-version: 24
- name: Set up Java 21
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
distribution: 'temurin'
java-version: '21'
Expand All @@ -57,11 +57,11 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Python 3.9
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: 3.9
- name: Install dependencies
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ jobs:

steps:
- name: Checkout source for staging
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.client_payload.ref || github.ref }}
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: 3.9

Expand Down Expand Up @@ -67,14 +67,14 @@ jobs:
# Attach the packaged artifacts to the workflow output. These can be manually
# downloaded for later inspection if necessary.
- name: Archive artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dist
path: dist

- name: Send email on failure
if: failure()
uses: firebase/firebase-admin-node/.github/actions/send-email@2e2b36a84ba28679bcb7aecdacabfec0bded2d48 # Admin Node SDK v13.6.0
uses: firebase/firebase-admin-node/.github/actions/send-email@7343f166ec71c2efee5fe0554c2fb96203faf2a5 # Admin Node SDK v14.2.0
with:
api-key: ${{ secrets.OSS_BOT_MAILGUN_KEY }}
domain: ${{ secrets.OSS_BOT_MAILGUN_DOMAIN }}
Expand All @@ -89,7 +89,7 @@ jobs:

- name: Send email on cancelled
if: cancelled()
uses: firebase/firebase-admin-node/.github/actions/send-email@2e2b36a84ba28679bcb7aecdacabfec0bded2d48 # Admin Node SDK v13.6.0
uses: firebase/firebase-admin-node/.github/actions/send-email@7343f166ec71c2efee5fe0554c2fb96203faf2a5 # Admin Node SDK v14.2.0
with:
api-key: ${{ secrets.OSS_BOT_MAILGUN_KEY }}
domain: ${{ secrets.OSS_BOT_MAILGUN_DOMAIN }}
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@

steps:
- name: Checkout source for staging
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: 3.9

Expand Down Expand Up @@ -83,7 +83,7 @@
# Attach the packaged artifacts to the workflow output. These can be manually
# downloaded for later inspection if necessary.
- name: Archive artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dist
path: dist
Expand All @@ -110,14 +110,14 @@

steps:
- name: Checkout source for publish
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Ignored since the GITHUB_TOKEN needs to be persisted for this workflow.
persist-credentials: true # zizmor: ignore[artipacked]

# Download the artifacts created by the stage_release job.
- name: Download release candidates
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: dist
path: dist
Expand All @@ -138,4 +138,4 @@
--notes "$RELEASE_BODY"

- name: Publish to Pypi
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0

Check failure on line 141 in .github/workflows/release.yml

View workflow job for this annotation

GitHub Actions / zizmor-output

zizmor/known-vulnerable-actions

action has a known vulnerability: GHSA-5xjx-3w67-w9x9

Check failure on line 141 in .github/workflows/release.yml

View workflow job for this annotation

GitHub Actions / zizmor-output

known-vulnerable-actions

release.yml:141: action has a known vulnerability: GHSA-5xjx-3w67-w9x9
2 changes: 1 addition & 1 deletion firebase_admin/__about__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

"""About information (version, etc) for Firebase Admin SDK."""

__version__ = '7.5.0'
__version__ = '7.7.0'
__title__ = 'firebase_admin'
__author__ = 'Firebase'
__license__ = 'Apache License 2.0'
Expand Down
3 changes: 3 additions & 0 deletions firebase_admin/_auth_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ def __init__(self, app, tenant_id=None):
2. set the project ID explicitly via Firebase App options, or
3. set the project ID via the GOOGLE_CLOUD_PROJECT environment variable.""")

if tenant_id is not None:
_auth_utils.validate_tenant_id(tenant_id)

credential = None
version_header = f'Python/Admin/{firebase_admin.__version__}'
timeout = app.options.get('httpTimeout', _http_client.DEFAULT_TIMEOUT_SECONDS)
Expand Down
15 changes: 8 additions & 7 deletions firebase_admin/_auth_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@ def __init__(self, http_client, project_id, tenant_id=None, url_override=None):
self.http_client = http_client
url_prefix = url_override or self.PROVIDER_CONFIG_URL
self.base_url = f'{url_prefix}/projects/{project_id}'
if tenant_id:
if tenant_id is not None:
_auth_utils.validate_tenant_id(tenant_id)
self.base_url += f'/tenants/{tenant_id}'

def get_oidc_provider_config(self, provider_id):
Expand Down Expand Up @@ -218,7 +219,7 @@ def create_oidc_provider_config(
if response_type:
req['responseType'] = response_type

params = f'oauthIdpConfigId={provider_id}'
params = {'oauthIdpConfigId': provider_id}
body = self._make_request('post', '/oauthIdpConfigs', json=req, params=params)
return OIDCProviderConfig(body)

Expand Down Expand Up @@ -301,7 +302,7 @@ def create_saml_provider_config(
if enabled is not None:
req['enabled'] = _auth_utils.validate_boolean(enabled, 'enabled')

params = f'inboundSamlConfigId={provider_id}'
params = {'inboundSamlConfigId': provider_id}
body = self._make_request('post', '/inboundSamlConfigs', json=req, params=params)
return SAMLProviderConfig(body)

Expand Down Expand Up @@ -369,9 +370,9 @@ def _fetch_provider_configs(self, path, page_token=None, max_results=MAX_LIST_CO
'Max results must be a positive integer less than or equal to '
f'{MAX_LIST_CONFIGS_RESULTS}.')

params = f'pageSize={max_results}'
params = {'pageSize': max_results}
if page_token:
params += f'&pageToken={page_token}'
params['pageToken'] = page_token
return self._make_request('get', path, params=params)

def _make_request(self, method, path, **kwargs):
Expand All @@ -386,7 +387,7 @@ def _validate_oidc_provider_id(provider_id):
if not isinstance(provider_id, str):
raise ValueError(
f'Invalid OIDC provider ID: {provider_id}. Provider ID must be a non-empty string.')
if not provider_id.startswith('oidc.'):
if not provider_id.startswith('oidc.') or not _auth_utils.is_safe_url_segment(provider_id):
raise ValueError(f'Invalid OIDC provider ID: {provider_id}.')
return provider_id

Expand All @@ -395,7 +396,7 @@ def _validate_saml_provider_id(provider_id):
if not isinstance(provider_id, str):
raise ValueError(
f'Invalid SAML provider ID: {provider_id}. Provider ID must be a non-empty string.')
if not provider_id.startswith('saml.'):
if not provider_id.startswith('saml.') or not _auth_utils.is_safe_url_segment(provider_id):
raise ValueError(f'Invalid SAML provider ID: {provider_id}.')
return provider_id

Expand Down
17 changes: 17 additions & 0 deletions firebase_admin/_auth_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
'iss', 'jti', 'nbf', 'nonce', 'sub', 'firebase',
])
VALID_EMAIL_ACTION_TYPES = set(['VERIFY_EMAIL', 'EMAIL_SIGNIN', 'PASSWORD_RESET'])
_SAFE_URL_SEGMENT_PATTERN = re.compile(r'^[a-zA-Z0-9\-_.~]+\Z')


class PageIterator:
Expand Down Expand Up @@ -83,6 +84,22 @@ def is_emulated():
return get_emulator_host() != ''


def is_safe_url_segment(segment):
if not isinstance(segment, str) or not segment or segment.endswith('.'):
return False
return bool(_SAFE_URL_SEGMENT_PATTERN.match(segment))


def validate_tenant_id(tenant_id):
if not isinstance(tenant_id, str) or not tenant_id:
raise ValueError(
f'Invalid tenant ID: {tenant_id}. Tenant ID must be a non-empty string.')
if not is_safe_url_segment(tenant_id):
raise ValueError(
f'Invalid tenant ID: {tenant_id}. Tenant ID must be a valid URL path segment.')
return tenant_id


def validate_uid(uid, required=False):
if uid is None and not required:
return None
Expand Down
3 changes: 2 additions & 1 deletion firebase_admin/_user_mgt.py
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,8 @@ def __init__(self, http_client, project_id, tenant_id=None, url_override=None):
self.http_client = http_client
url_prefix = url_override or self.ID_TOOLKIT_URL
self.base_url = f'{url_prefix}/projects/{project_id}'
if tenant_id:
if tenant_id is not None:
_auth_utils.validate_tenant_id(tenant_id)
self.base_url += f'/tenants/{tenant_id}'

def get_user(self, **kwargs):
Expand Down
62 changes: 53 additions & 9 deletions firebase_admin/app_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,38 +15,47 @@
"""Firebase App Check module."""

from typing import Any, Dict
import requests
import jwt
from jwt import PyJWKClient, ExpiredSignatureError, InvalidTokenError, DecodeError
from jwt import InvalidAudienceError, InvalidIssuerError, InvalidSignatureError
from firebase_admin import _utils
from firebase_admin import _http_client, _utils, exceptions

_APP_CHECK_ATTRIBUTE = '_app_check'

def _get_app_check_service(app) -> Any:
return _utils.get_app_service(app, _APP_CHECK_ATTRIBUTE, _AppCheckService)

def verify_token(token: str, app=None) -> Dict[str, Any]:
"""Verifies a Firebase App Check token.
def verify_token(token: str, app=None, consume: bool = False) -> Dict[str, Any]:
"""Verifies a Firebase App Check token, optionally consuming limited-use tokens.

Args:
token: A token from App Check.
app: An App instance (optional).
consume: Set to ``True`` only if the token is a limited-use (one-time) token
that should be consumed upon verification (optional, defaults to ``False``).

Returns:
Dict[str, Any]: The token's decoded claims.
Dict[str, Any]: The token's decoded claims. If ``consume`` is ``True``, the dictionary
also includes an ``already_consumed`` boolean key indicating whether the token was
previously consumed.

Raises:
ValueError: If the app's ``project_id`` is invalid or unspecified,
or if the token's headers or payload are invalid.
ValueError: If ``consume`` is not a boolean, or if the app's ``project_id``
is invalid or unspecified, or if the token's headers or payload are invalid.
FirebaseError: If an error occurs while communicating with the App Check service.
PyJWKClientError: If PyJWKClient fails to fetch a valid signing key.
"""
return _get_app_check_service(app).verify_token(token)
return _get_app_check_service(app).verify_token(token, consume=consume)

class _AppCheckService:
"""Service class that implements Firebase App Check functionality."""

_APP_CHECK_ISSUER = 'https://firebaseappcheck.googleapis.com/'
_JWKS_URL = 'https://firebaseappcheck.googleapis.com/v1/jwks'
_VERIFY_URL_FORMAT = (
'https://firebaseappcheck.googleapis.com/v1beta/projects/{project_id}:verifyAppCheckToken'
)
_project_id = None
_scoped_project_id = None
_jwks_client = None
Expand All @@ -68,11 +77,15 @@ def __init__(self, app):
# Default lifespan is 300 seconds (5 minutes) so we change it to 21600 seconds (6 hours).
self._jwks_client = PyJWKClient(
self._JWKS_URL, lifespan=21600, headers=self._APP_CHECK_HEADERS)
timeout = app.options.get('httpTimeout', _http_client.DEFAULT_TIMEOUT_SECONDS)
self._http_client = _http_client.JsonHttpClient(
credential=app.credential.get_credential(), timeout=timeout)


def verify_token(self, token: str) -> Dict[str, Any]:
"""Verifies a Firebase App Check token."""
def verify_token(self, token: str, consume: bool = False) -> Dict[str, Any]:
"""Verifies a Firebase App Check token, optionally consuming limited-use tokens."""
_Validators.check_string("app check token", token)
_Validators.check_boolean("consume", consume)

# Obtain the Firebase App Check Public Keys
# Note: It is not recommended to hard code these keys as they rotate,
Expand All @@ -87,8 +100,32 @@ def verify_token(self, token: str) -> Dict[str, Any]:
) from exception

verified_claims['app_id'] = verified_claims.get('sub')

if consume:
verified_claims['already_consumed'] = self._verify_replay_protection(token)

return verified_claims

def _verify_replay_protection(self, token: str) -> bool:
"""Verifies replay protection with the backend and returns the alreadyConsumed status."""
url = self._VERIFY_URL_FORMAT.format(project_id=self._project_id)
try:
body = self._http_client.body('post', url, json={'app_check_token': token})
except requests.exceptions.RequestException as error:
raise _utils.handle_platform_error_from_requests(error)
except ValueError as error:
raise exceptions.UnknownError(
f'Unexpected response from App Check service: {error}'
) from error

if not isinstance(body, dict):
raise exceptions.UnknownError(
'Unexpected response from App Check service. '
f'Expected a JSON object, but got {type(body).__name__}.'
)

return bool(body.get('alreadyConsumed', False))

def _has_valid_token_headers(self, headers: Any) -> None:
"""Checks whether the token has valid headers for App Check."""
# Ensure the token's header has type JWT
Expand Down Expand Up @@ -159,3 +196,10 @@ def check_string(cls, label: str, value: Any):
raise ValueError(f'{label} "{value}" must be a non-empty string.')
if not isinstance(value, str):
raise ValueError(f'{label} "{value}" must be a string.')

@classmethod
def check_boolean(cls, label: str, value: Any):
"""Checks if the given value is a boolean."""
if not isinstance(value, bool):
raise ValueError(f'{label} must be a boolean.')
return value
Loading
Loading