Wildcard origins follow the shell-glob convention: **. for apex+any depth, *. for one level
'**.example.com' covers the apex and subdomains at any depth;
'*.example.com' covers exactly one subdomain level (neither apex nor
deeper) — analogous to permission scope wildcards, and sidestepping the
DNS/TLS/nginx ambiguity around '*.'. This also allows excluding the apex
where wanted. The seeded/default entry becomes '**.{rp-id}' (init,
add-domain, legacy empty-origins conversion, branch-era '*' sanitize
rewrite).
This commit is contained in:
+1
-1
@@ -96,7 +96,7 @@ async def test_db() -> AsyncGenerator[DB]:
|
||||
admin_name="Test Admin",
|
||||
config=Config(
|
||||
domains={
|
||||
TEST_RP_ID: DomainConfig(origins={f"*.{TEST_RP_ID}": True})
|
||||
TEST_RP_ID: DomainConfig(origins={f"**.{TEST_RP_ID}": True})
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
+2
-2
@@ -1828,7 +1828,7 @@ class TestDomains:
|
||||
assert len(data) == 1
|
||||
domain = data[0]
|
||||
assert domain["rp_id"] == "localhost"
|
||||
assert domain["origins"] == {"*.localhost": True}
|
||||
assert domain["origins"] == {"**.localhost": True}
|
||||
assert "related" not in domain
|
||||
assert domain["auth_host"] is None
|
||||
assert domain["site_url"] == "http://localhost:4401"
|
||||
@@ -2022,7 +2022,7 @@ class TestDomains:
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
# Plain '*' is rejected — wildcards must be explicit ('*.another.com')
|
||||
# Plain '*' is rejected — wildcards must be explicit ('**.another.com')
|
||||
r = await client.post(
|
||||
"/auth/api/admin/domains/",
|
||||
json={"rp_id": "star.com", "origins": {"*": True}},
|
||||
|
||||
+2
-2
@@ -87,7 +87,7 @@ def test_init_defaults(run_cli, tmp_path):
|
||||
config = stored_config(tmp_path)
|
||||
assert list(config.domains) == ["localhost"]
|
||||
assert config.domains["localhost"].rp_name is None
|
||||
assert config.domains["localhost"].origins == {"*.localhost": True}
|
||||
assert config.domains["localhost"].origins == {"**.localhost": True}
|
||||
assert config.listen is None
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ def test_init_full_options(run_cli, tmp_path):
|
||||
config = stored_config(tmp_path)
|
||||
domain = config.domains["example.com"]
|
||||
assert domain.rp_name == "Example Corp"
|
||||
assert domain.origins == {"*.example.com": True}
|
||||
assert domain.origins == {"**.example.com": True}
|
||||
assert config.listen == ["4402"]
|
||||
|
||||
|
||||
|
||||
+36
-17
@@ -59,7 +59,7 @@ ROR_CONFIG = Config(
|
||||
"app.com": True, # related origin (outside the rp-id domain)
|
||||
},
|
||||
),
|
||||
"pro.com": DomainConfig(rp_name="Pro", origins={"*.pro.com": True}),
|
||||
"pro.com": DomainConfig(rp_name="Pro", origins={"**.pro.com": True}),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -232,13 +232,16 @@ class TestValidateConfig:
|
||||
|
||||
def test_wildcard_outside_rp_id_rejected(self):
|
||||
"""Related origins are individual hosts; wildcards must stay within
|
||||
the rp-id domain."""
|
||||
the rp-id domain. Both wildcard forms are accepted in-domain."""
|
||||
domains.validate_config(
|
||||
Config(domains={"a.com": DomainConfig(origins={"*.a.com": True})})
|
||||
)
|
||||
domains.validate_config(
|
||||
Config(domains={"a.com": DomainConfig(origins={"**.a.com": True})})
|
||||
)
|
||||
with pytest.raises(ValueError, match="wildcard outside the rp-id"):
|
||||
domains.validate_config(
|
||||
Config(domains={"a.com": DomainConfig(origins={"*.b.com": True})})
|
||||
Config(domains={"a.com": DomainConfig(origins={"**.b.com": True})})
|
||||
)
|
||||
|
||||
def test_wildcard_auth_host_rejected(self):
|
||||
@@ -365,13 +368,13 @@ class TestSanitizeConfig:
|
||||
domains.validate_config(config) # sanitized config is strict-clean
|
||||
|
||||
def test_star_origin_rewritten_explicit(self):
|
||||
"""Branch-era '*' shorthand is rewritten to '*.{rp-id}'; an auth
|
||||
"""Branch-era '*' shorthand is rewritten to '**.{rp-id}'; an auth
|
||||
mark on it is cleared."""
|
||||
config, warnings = domains.sanitize_config(
|
||||
Config(domains={"a.com": DomainConfig(origins={"*": True})})
|
||||
)
|
||||
assert config.domains["a.com"].origins == {"*.a.com": True}
|
||||
assert any("'*.'" in w or "*." in w for w in warnings)
|
||||
assert config.domains["a.com"].origins == {"**.a.com": True}
|
||||
assert any("**." in w for w in warnings)
|
||||
domains.validate_config(config)
|
||||
|
||||
config, warnings = domains.sanitize_config(
|
||||
@@ -381,7 +384,7 @@ class TestSanitizeConfig:
|
||||
}
|
||||
)
|
||||
)
|
||||
assert config.domains["a.com"].origins == {"*.a.com": True}
|
||||
assert config.domains["a.com"].origins == {"**.a.com": True}
|
||||
assert any("mark cleared" in w for w in warnings)
|
||||
domains.validate_config(config)
|
||||
|
||||
@@ -521,16 +524,31 @@ class TestOriginValidation:
|
||||
with pytest.raises(ValueError, match="not allowed"):
|
||||
p.validate_origin("https://other.com")
|
||||
|
||||
def test_wildcard_entry_matches_subtree(self):
|
||||
p = Passkey(rp_id="example.com", origins=["*.example.com"])
|
||||
assert p.validate_origin("https://example.com")
|
||||
assert p.validate_origin("https://app.example.com")
|
||||
def test_double_star_matches_apex_and_any_depth(self):
|
||||
"""'**.example.com' covers the apex and subdomains at any depth
|
||||
(the shell-glob convention)."""
|
||||
p = Passkey(rp_id="example.com", origins=["**.example.com"])
|
||||
assert p.validate_origin("https://example.com") # apex
|
||||
assert p.validate_origin("https://app.example.com") # one level
|
||||
assert p.validate_origin("https://a.b.c.example.com") # any depth
|
||||
with pytest.raises(ValueError, match="not allowed"):
|
||||
p.validate_origin("https://anotherexample.com")
|
||||
with pytest.raises(ValueError, match="not allowed"):
|
||||
p.validate_origin("https://other.com")
|
||||
|
||||
def test_wildcard_is_https_only(self):
|
||||
"""A '*.example.com' entry does not fall back to other schemes."""
|
||||
def test_single_star_matches_exactly_one_level(self):
|
||||
"""'*.example.com' covers exactly one subdomain level — neither the
|
||||
apex nor deeper levels."""
|
||||
p = Passkey(rp_id="example.com", origins=["*.example.com"])
|
||||
assert p.validate_origin("https://app.example.com")
|
||||
with pytest.raises(ValueError, match="not allowed"):
|
||||
p.validate_origin("https://example.com") # apex excluded
|
||||
with pytest.raises(ValueError, match="not allowed"):
|
||||
p.validate_origin("https://a.b.example.com") # too deep
|
||||
|
||||
def test_wildcard_is_https_only(self):
|
||||
"""A '**.example.com' entry does not fall back to other schemes."""
|
||||
p = Passkey(rp_id="example.com", origins=["**.example.com"])
|
||||
with pytest.raises(ValueError, match="not allowed"):
|
||||
p.validate_origin("http://example.com")
|
||||
with pytest.raises(ValueError, match="not allowed"):
|
||||
@@ -542,9 +560,10 @@ class TestOriginValidation:
|
||||
|
||||
def test_localhost_wildcard_matches_any_scheme_and_port(self):
|
||||
"""Under localhost, wildcards match any scheme and any port."""
|
||||
p = Passkey(rp_id="localhost", origins=["*.localhost"])
|
||||
p = Passkey(rp_id="localhost", origins=["**.localhost"])
|
||||
assert p.validate_origin("http://localhost:8080")
|
||||
assert p.validate_origin("http://app.localhost:3000")
|
||||
assert p.validate_origin("http://a.b.localhost:3000")
|
||||
assert p.validate_origin("https://localhost")
|
||||
|
||||
def test_exact_entry_matches_scheme_and_port(self):
|
||||
@@ -556,7 +575,7 @@ class TestOriginValidation:
|
||||
p.validate_origin("http://localhost:4404")
|
||||
|
||||
def test_sub_wildcard_matches_only_its_subtree(self):
|
||||
p = Passkey(rp_id="example.com", origins=["*.app.example.com"])
|
||||
p = Passkey(rp_id="example.com", origins=["**.app.example.com"])
|
||||
assert p.validate_origin("https://app.example.com")
|
||||
assert p.validate_origin("https://www.app.example.com")
|
||||
with pytest.raises(ValueError, match="not allowed"):
|
||||
@@ -870,13 +889,13 @@ class TestLegacyConversion:
|
||||
|
||||
def test_convert_empty_origins_seeds_wildcard(self, tmp_path):
|
||||
"""Legacy 'no origins' meant the whole rp-id domain; the new format
|
||||
makes that explicit as '*.{rp-id}'."""
|
||||
makes that explicit as '**.{rp-id}'."""
|
||||
src_file = tmp_path / "main.db"
|
||||
asyncio.run(
|
||||
_write_legacy(src_file, LegacyConfig(rp_id="example.com", rp_name="Ex"))
|
||||
)
|
||||
config = convert_legacy_database(src_file, tmp_path / "paskia.kantadb")
|
||||
assert config.domains["example.com"].origins == {"*.example.com": True}
|
||||
assert config.domains["example.com"].origins == {"**.example.com": True}
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user