Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 4 additions & 2 deletions PARITY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# PHP client parity

This Python client implements the current PHP identity contract (`id` and `email`), following PHP commit `bcb547d`. The language API uses snake_case methods, frozen dataclass user snapshots and request-scoped mappings.
This Python client implements the current PHP identity contract (`id` and `email`), following PHP commit `4dcc01fe5d07933400c82695bf9574097ee19d43`. The language API uses snake_case methods, frozen dataclass user snapshots and request-scoped mappings.

| PHP source | Python equivalent |
| --- | --- |
Expand Down Expand Up @@ -35,7 +35,7 @@ This Python client implements the current PHP identity contract (`id` and `email
- Logout removes only authentication state before redirect. Explicit destinations override the configured return URI, then the provider default. Query-only destinations replace the current query.
- Fake login stores synthetic data, appends its own response query and defers refreshing the current user until the redirected request. Only the default fake email takes the local logout branch.
- The low-level token decoder uses the request IV and accepts `uuid` or `id`; the normal response path uses the secret session IV and `id`. Extra fields are ignored, matching the current PHP client.
- Administration links point to `/admin/` and do not grant access. HTTPS is required except for `localhost` and the literal `127.0.0.127`.
- Administration links point to `/admin/` and do not grant access. HTTPS is required except for `localhost`, the literal `127.0.0.127`, and HTTP providers with a `.test` hostname suffix.
- The unused legacy session-store constant and deprecated user alias/getter are retained. Actual storage uses `Authwave\SessionData`.

## Runtime adaptations
Expand All @@ -47,3 +47,5 @@ Redirects transfer control to adapters, which save before sending a 303. Applica
PHP dependency APIs such as the entire PSR-7 URI interface are not reimplemented; native language URI types/helpers provide the operations needed by the client. PHP's diagnostic logging and warning mechanisms use the host language's conventions. Browser login against a deployed provider is separate from the automated simulated-provider tests.

Python emits `DeprecationWarning` for `get_user()`. Default redirects raise `RedirectSignal`. Optional Flask and Django decorators and ASGI/WSGI middleware buffer responses and use opaque cookies. FastAPI uses the ASGI middleware. Server-side store methods are synchronous; ASGI executes store operations and constructor callbacks in worker threads.

Provider HTTP policy also allows the `.test` hostname suffix (case-insensitive, ignoring trailing DNS dots), for local test providers. The exception does not allow other schemes.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ python -m build
Tests include 68 deterministic PHP-reference scenarios and complete framework login/logout flows using simulated provider responses. To also execute the PHP source and check the fixtures, set `AUTHWAVE_PHP_REFERENCE` to the PHP checkout with installed Composer dependencies.

[PARITY.md](PARITY.md) maps the PHP API to its Python equivalents.

For a local test provider, HTTP is supported for hosts ending in `.test`, such as `http://login.realtest.authwave.test:9090`. Configure local DNS or a hosts-file entry for the provider. This exception applies only to HTTP and the complete `.test` hostname suffix; `.authwave-test` and names such as `example.test.com` do not qualify. Other remote providers require HTTPS. Use test accounts on a trusted development network; the suffix does not encrypt traffic or guarantee that it stays on your machine.
4 changes: 4 additions & 0 deletions authwave/uri.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,12 @@ def normalize_base_uri(base: str) -> str:
parts = urlsplit(base if "://" in base else "https://" + base)
if not parts.hostname:
raise ValueError("Provider hostname is required")
is_test_http = parts.scheme == "http" and parts.hostname.lower().rstrip(
"."
).endswith(".test")
if (
parts.hostname not in ("localhost", "127.0.0.127")
and not is_test_http
and parts.scheme != "https"
):
raise InsecureProtocolException(parts.scheme)
Expand Down
33 changes: 33 additions & 0 deletions tests/test_test_domains.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import pytest
from authwave import AdminUri, InsecureProtocolException


@pytest.mark.parametrize(
"host,allowed",
[
("http://login.realtest.authwave.test:9090", True),
("http://app.test", True),
("http://LOGIN.EXAMPLE.TEST:9090", True),
("http://login.example.test.:9090", True),
("http://localhost:9090", True),
("http://127.0.0.127:9090", True),
("https://login.authwave.com", True),
("http://login.realtest.authwave-test:9090", False),
("http://example.test.attacker.com", False),
("http://exampletest", False),
("http://test", False),
("http://login.authwave.com", False),
("ftp://example.test", False),
],
)
def test_provider_protocol_policy(host, allowed):
if not allowed:
with pytest.raises(InsecureProtocolException):
AdminUri(host)
return
uri = AdminUri(host)
assert uri.get_path() == "/admin/"
if host.startswith("http://"):
assert uri.get_scheme() == "http"
if ":9090" in host:
assert uri.get_port() == 9090
Loading