fix: activation WebAuthn impossible BUG-070 (rp_id/origines derives requete, challenges multiples)
This commit is contained in:
+130
-2
@@ -134,8 +134,8 @@ class TestWebauthnModule:
|
||||
w._pending.clear()
|
||||
w._store_challenge("u2:register")
|
||||
key = "u2:register"
|
||||
ch, _ = w._pending[key]
|
||||
w._pending[key] = (ch, _t.time() - 1)
|
||||
ch, _ = w._pending[key][0]
|
||||
w._pending[key] = [(ch, _t.time() - 1)]
|
||||
assert w._take_challenge(key) is None
|
||||
|
||||
def test_full_registration_and_authentication_roundtrip(self):
|
||||
@@ -337,3 +337,131 @@ class TestWebauthnApi:
|
||||
assert r2.status_code == 200
|
||||
st = wa_client.get("/api/auth/mfa/status", headers=headers).json()
|
||||
assert st["mfa_enabled"] is False
|
||||
|
||||
|
||||
# ── BUG-070: relying party derived from the request ─────────────────────
|
||||
#
|
||||
# The old defaults (rp_id "localhost", origins ["http://localhost"]) rejected
|
||||
# every real access URL: "Unexpected client data origin
|
||||
# "http://localhost:2020", expected one of ['http://localhost']".
|
||||
|
||||
def _fake_request(host, scheme="http", forwarded_host=None, forwarded_proto=None):
|
||||
from fastapi import Request
|
||||
|
||||
headers = [(b"host", host.encode())]
|
||||
if forwarded_host is not None:
|
||||
headers.append((b"x-forwarded-host", forwarded_host.encode()))
|
||||
if forwarded_proto is not None:
|
||||
headers.append((b"x-forwarded-proto", forwarded_proto.encode()))
|
||||
return Request({
|
||||
"type": "http", "method": "POST", "path": "/",
|
||||
"headers": headers, "scheme": scheme,
|
||||
"server": ("testserver", 80), "client": ("127.0.0.1", 5000),
|
||||
})
|
||||
|
||||
|
||||
class TestRelyingPartyResolution:
|
||||
def test_defaults_without_request(self, monkeypatch):
|
||||
import backend.auth.webauthn_mfa as w
|
||||
|
||||
monkeypatch.delenv("OBSIGATE_WEBAUTHN_RP_ID", raising=False)
|
||||
monkeypatch.delenv("OBSIGATE_WEBAUTHN_ORIGINS", raising=False)
|
||||
assert w.resolve_relying_party(None) == ("localhost", ["http://localhost"])
|
||||
|
||||
def test_derives_host_with_port(self, monkeypatch):
|
||||
"""Exact BUG-070 report: http://localhost:2020 was rejected."""
|
||||
import backend.auth.webauthn_mfa as w
|
||||
|
||||
monkeypatch.delenv("OBSIGATE_WEBAUTHN_RP_ID", raising=False)
|
||||
monkeypatch.delenv("OBSIGATE_WEBAUTHN_ORIGINS", raising=False)
|
||||
rp, origins = w.resolve_relying_party(_fake_request("localhost:2020"))
|
||||
assert rp == "localhost"
|
||||
assert origins == ["http://localhost:2020"]
|
||||
|
||||
def test_derives_ip_host(self, monkeypatch):
|
||||
import backend.auth.webauthn_mfa as w
|
||||
|
||||
monkeypatch.delenv("OBSIGATE_WEBAUTHN_RP_ID", raising=False)
|
||||
monkeypatch.delenv("OBSIGATE_WEBAUTHN_ORIGINS", raising=False)
|
||||
rp, origins = w.resolve_relying_party(_fake_request("127.0.0.1:2020"))
|
||||
assert rp == "127.0.0.1"
|
||||
assert origins == ["http://127.0.0.1:2020"]
|
||||
|
||||
def test_explicit_env_wins_over_request(self, monkeypatch):
|
||||
import backend.auth.webauthn_mfa as w
|
||||
|
||||
monkeypatch.setenv("OBSIGATE_WEBAUTHN_RP_ID", "obs.example.com")
|
||||
monkeypatch.setenv("OBSIGATE_WEBAUTHN_ORIGINS",
|
||||
"https://obs.example.com, https://www.obs.example.com")
|
||||
rp, origins = w.resolve_relying_party(_fake_request("localhost:2020"))
|
||||
assert rp == "obs.example.com"
|
||||
assert origins == ["https://obs.example.com",
|
||||
"https://www.obs.example.com"]
|
||||
|
||||
def test_forwarded_headers_require_trust(self, monkeypatch):
|
||||
import backend.auth.webauthn_mfa as w
|
||||
|
||||
monkeypatch.delenv("OBSIGATE_WEBAUTHN_RP_ID", raising=False)
|
||||
monkeypatch.delenv("OBSIGATE_WEBAUTHN_ORIGINS", raising=False)
|
||||
monkeypatch.setenv("OBSIGATE_TRUST_PROXY", "false")
|
||||
req = _fake_request("internal:8080", scheme="http",
|
||||
forwarded_host="obs.example.com",
|
||||
forwarded_proto="https")
|
||||
assert w.resolve_relying_party(req) == ("internal", ["http://internal:8080"])
|
||||
|
||||
monkeypatch.setenv("OBSIGATE_TRUST_PROXY", "true")
|
||||
assert w.resolve_relying_party(req) == ("obs.example.com",
|
||||
["https://obs.example.com"])
|
||||
|
||||
def test_hostname_only(self):
|
||||
import backend.auth.webauthn_mfa as w
|
||||
|
||||
assert w._hostname_only("example.com:2020") == "example.com"
|
||||
assert w._hostname_only("example.com") == "example.com"
|
||||
assert w._hostname_only("[::1]:8080") == "::1"
|
||||
assert w._hostname_only("127.0.0.1:2020") == "127.0.0.1"
|
||||
|
||||
def test_retry_after_reoptions_still_verifies(self):
|
||||
"""A re-requested options call (double-click) must not kill the
|
||||
in-flight ceremony: "challenge was not expected challenge"."""
|
||||
import backend.auth.webauthn_mfa as w
|
||||
|
||||
w._pending.clear()
|
||||
auth = VirtualAuthenticator()
|
||||
first = w._store_challenge("bob:register")
|
||||
w._store_challenge("bob:register") # second options call overwrites
|
||||
cred = auth.make_registration({"challenge": _b64url(first)})
|
||||
rec = w.complete_registration("bob", cred, rp_id_override="localhost",
|
||||
origins_override=["http://localhost"])
|
||||
assert rec["credential_id"] == cred["id"]
|
||||
|
||||
def test_register_flow_without_env_config(self, wa_client, monkeypatch):
|
||||
"""Full register + login roundtrip with no WEBAUTHN env at all: the
|
||||
relying party derives from the request (TestClient host)."""
|
||||
import backend.auth.webauthn_mfa as w
|
||||
|
||||
monkeypatch.delenv("OBSIGATE_WEBAUTHN_RP_ID", raising=False)
|
||||
monkeypatch.delenv("OBSIGATE_WEBAUTHN_ORIGINS", raising=False)
|
||||
w._pending.clear()
|
||||
|
||||
headers = _login_headers(wa_client)
|
||||
r = wa_client.post("/api/auth/mfa/webauthn/register/options", headers=headers)
|
||||
assert r.status_code == 200
|
||||
options = r.json()["options"]
|
||||
assert options["rp"]["id"] == "testserver"
|
||||
|
||||
auth = VirtualAuthenticator()
|
||||
auth.RP_ID = "testserver"
|
||||
auth.ORIGIN = "http://testserver"
|
||||
cred = auth.make_registration(options)
|
||||
r2 = wa_client.post("/api/auth/mfa/webauthn/register", headers=headers,
|
||||
json={"credential": cred, "label": "Key"})
|
||||
assert r2.status_code == 200, r2.text
|
||||
|
||||
opts_r = wa_client.post("/api/auth/mfa/webauthn/options",
|
||||
json={"username": "testuser"})
|
||||
assertion = auth.make_assertion(opts_r.json()["options"])
|
||||
v = wa_client.post("/api/auth/mfa/webauthn/verify",
|
||||
json={"username": "testuser", "credential": assertion})
|
||||
assert v.status_code == 200, v.text
|
||||
assert "access_token" in v.json()
|
||||
|
||||
Reference in New Issue
Block a user