Skip to content

Commit 61614c9

Browse files
committed
refactor(identity): match blocking token audiences on host suffix only
iss already binds the token to the project, so aud only needs to tell blocking tokens apart from regular ID tokens. Drops the region regex.
1 parent 16fd338 commit 61614c9

2 files changed

Lines changed: 24 additions & 50 deletions

File tree

‎src/firebase_functions/private/token_verifier.py‎

Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
Module for internal token verification.
1616
"""
1717

18-
import re
1918
from urllib.parse import urlsplit
2019

2120
import google.auth.exceptions
@@ -32,41 +31,25 @@
3231
)
3332
from google.auth import jwt
3433

35-
_CLOUD_RUN_HOST_SUFFIX = ".run.app"
34+
_BLOCKING_HOST_SUFFIXES = (".run.app", ".cloudfunctions.net")
3635

3736

38-
def _blocking_audience_matcher(project_id):
39-
"""Builds the `audience_matcher` for auth blocking tokens.
37+
def _blocking_audience_matcher(audience):
38+
"""The `audience_matcher` for auth blocking tokens.
4039
4140
A blocking token's `aud` is the function's own URL. firebase-tools registers the
4241
cloudfunctions.net URL when it creates a blocking function and the run.app URL
4342
when it updates one, so either host has to be accepted.
4443
45-
The gen-1 host is matched as `<region>-<project-id>.cloudfunctions.net` with the
46-
region pinned to its naming shape, so a project id that merely ends with this one
47-
(`other-my-project` against `my-project`) is not accepted as a suffix. Matching
48-
the parsed host rather than a substring of the URL also keeps the expected text
49-
from being smuggled in via a path, query or fragment.
50-
51-
Deliberately stricter than firebase-admin-node, which substring-matches `aud`.
52-
53-
The run.app host is not project-scoped: any Cloud Run URL satisfies it, as in the
54-
Node SDK. The `iss` check is what ties the token to this project.
44+
Neither host is project-scoped: `iss` ties the token to this project. The `aud`
45+
check only has to separate blocking tokens from regular ID tokens, whose `aud` is
46+
the bare project id.
5547
"""
56-
cloudfunctions_host = re.compile(
57-
rf"[a-z]+(?:-[a-z]+)*\d+-{re.escape(project_id)}\.cloudfunctions\.net"
58-
)
59-
60-
def matches(audience):
61-
if not isinstance(audience, str):
62-
return False
63-
parts = urlsplit(audience)
64-
if parts.scheme != "https":
65-
return False
66-
host = parts.hostname or ""
67-
return host.endswith(_CLOUD_RUN_HOST_SUFFIX) or bool(cloudfunctions_host.fullmatch(host))
68-
69-
return matches
48+
if not isinstance(audience, str):
49+
return False
50+
parts = urlsplit(audience)
51+
host = parts.hostname or ""
52+
return parts.scheme == "https" and host.endswith(_BLOCKING_HOST_SUFFIXES)
7053

7154

7255
# pylint: disable=consider-using-f-string
@@ -229,11 +212,8 @@ def __init__(self, app):
229212
issuer=_token_gen.ID_TOKEN_ISSUER_PREFIX,
230213
invalid_token_error=InvalidAuthBlockingTokenError,
231214
expired_token_error=ExpiredAuthBlockingTokenError,
232-
audience_matcher=_blocking_audience_matcher(app.project_id),
233-
expected_audience_msg=(
234-
"a https://*.run.app or "
235-
f"https://<region>-{app.project_id}.cloudfunctions.net/ function URL"
236-
),
215+
audience_matcher=_blocking_audience_matcher,
216+
expected_audience_msg="a https://*.run.app or https://*.cloudfunctions.net function URL",
237217
)
238218

239219
def verify_auth_blocking_token(self, auth_blocking_token):

‎tests/test_token_verifier.py‎

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,7 @@ def _verify(app, token: str):
101101
# hash, and a revision tag prefixes the service with `tag---`.
102102
"https://beforecreate-123456789.us-central1.run.app",
103103
"https://tag---beforecreate-123456789.us-central1.run.app",
104-
# Regions the SupportedRegion enum predates, and a hypothetical shape with
105-
# more than one hyphen, since the region is matched by shape.
106104
f"https://northamerica-northeast1-{PROJECT_ID}.cloudfunctions.net/before_create",
107-
f"https://me-west1-{PROJECT_ID}.cloudfunctions.net/before_create",
108-
f"https://us-far-west1-{PROJECT_ID}.cloudfunctions.net/before_create",
109105
],
110106
)
111107
def test_accepts_function_url_audiences(app, signing_key, audience):
@@ -115,12 +111,9 @@ def test_accepts_function_url_audiences(app, signing_key, audience):
115111
@pytest.mark.parametrize(
116112
"audience",
117113
[
118-
"https://us-east1-other-project.cloudfunctions.net/before_create",
114+
# A regular Firebase ID token for the same project.
115+
PROJECT_ID,
119116
"https://example.com/before_create",
120-
# A project id ending with this one must not be accepted as a suffix.
121-
f"https://us-east1-other-{PROJECT_ID}.cloudfunctions.net/before_create",
122-
f"https://us-east1-x1-{PROJECT_ID}.cloudfunctions.net/before_create",
123-
f"https://us-east1-a-{PROJECT_ID}.cloudfunctions.net/before_create",
124117
# The expected host must be the host, not text anywhere in the URL.
125118
f"https://us-east1-{PROJECT_ID}.cloudfunctions.net.example.com/before_create",
126119
f"https://example.com/{PROJECT_ID}.cloudfunctions.net/before_create",
@@ -135,15 +128,16 @@ def test_rejects_foreign_audience(app, signing_key, audience):
135128
_verify(app, _token(signing_key, audience))
136129

137130

138-
def test_accepts_any_run_host(app, signing_key):
139-
"""The run.app form is not project-scoped; `iss` is what pins the project."""
140-
assert _verify(app, _token(signing_key, "https://svc-999.us-central1.run.app"))["uid"] == (
141-
"uid123"
131+
@pytest.mark.parametrize(
132+
"other_project",
133+
["other-project", f"other-{PROJECT_ID}", f"x1-{PROJECT_ID}", f"a-{PROJECT_ID}"],
134+
)
135+
def test_rejects_function_url_from_another_project(app, signing_key, other_project):
136+
token = _token(
137+
signing_key,
138+
f"https://us-east1-{other_project}.cloudfunctions.net/before_create",
139+
issuer=f"https://securetoken.google.com/{other_project}",
142140
)
143-
144-
145-
def test_rejects_wrong_issuer(app, signing_key):
146-
token = _token(signing_key, RUN_APP_AUDIENCE, issuer="https://securetoken.google.com/other")
147141
with pytest.raises(token_verifier.InvalidAuthBlockingTokenError, match='"iss"'):
148142
_verify(app, token)
149143

0 commit comments

Comments
 (0)