Skip to content
Merged
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
27 changes: 19 additions & 8 deletions projects/policyengine-api-simulation/src/modal/gateway/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

- ``GATEWAY_AUTH_ISSUER`` - Auth0 issuer URL (must end with ``/``)
- ``GATEWAY_AUTH_AUDIENCE`` - Auth0 API identifier the gateway accepts
- ``GATEWAY_AUTH_REQUIRED`` - if truthy, missing issuer/audience is a 503
- ``GATEWAY_AUTH_REQUIRED`` - if truthy, bearer JWT auth is enforced

For local development and unit tests the dependency can be bypassed by
setting ``GATEWAY_AUTH_DISABLED=1``. This bypass is hard-gated by
Expand All @@ -21,8 +21,8 @@
missing or looks like production, and otherwise requires an explicit
``GATEWAY_AUTH_DISABLED_ACK=I_UNDERSTAND_THIS_IS_DEV`` acknowledgement so
the bypass cannot be activated by a single stray env var. The gateway
also returns ``503`` to callers if auth is required but the issuer/audience
configuration is missing, or if only one of issuer/audience is present.
also returns ``503`` to callers if only one of issuer/audience is present, or
if auth is required but issuer/audience are missing.
"""

from __future__ import annotations
Expand Down Expand Up @@ -193,18 +193,29 @@ def require_auth(
missing or invalid token produces a 403 (matching the underlying
decoder's contract).

If issuer/audience env configuration is absent, the dependency preserves
the legacy public gateway behavior unless ``GATEWAY_AUTH_REQUIRED`` is
truthy. Partial auth configuration always returns 503 because it indicates
an operator intended to enable auth but shipped an incomplete secret.
The gateway preserves its legacy public behavior unless
``GATEWAY_AUTH_REQUIRED`` is truthy. Issuer/audience may be staged in the
Modal secret ahead of enforcement; setting those values alone must not
silently make the gateway private. Partial auth configuration always
returns 503 because it indicates an incomplete secret.
"""

if _auth_disabled():
return None

issuer = os.environ.get(GATEWAY_AUTH_ISSUER_ENV)
audience = os.environ.get(GATEWAY_AUTH_AUDIENCE_ENV)
if not issuer and not audience and not _auth_required():
if bool(issuer) != bool(audience):
logger.error(
"Gateway auth partially configured: issuer_present=%s audience_present=%s",
bool(issuer),
bool(audience),
)
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Gateway authentication is not configured.",
)
if not _auth_required():
return None

try:
Expand Down
13 changes: 13 additions & 0 deletions projects/policyengine-api-simulation/tests/gateway/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def __call__(self, token):

monkeypatch.setattr(auth_module, "_get_decoder", lambda: FailingDecoder())
monkeypatch.delenv(auth_module.GATEWAY_AUTH_DISABLED_ENV, raising=False)
monkeypatch.setenv(auth_module.GATEWAY_AUTH_REQUIRED_ENV, "1")
monkeypatch.setenv(auth_module.GATEWAY_AUTH_ISSUER_ENV, "https://issuer.example/")
monkeypatch.setenv(auth_module.GATEWAY_AUTH_AUDIENCE_ENV, "aud")

Expand Down Expand Up @@ -84,6 +85,17 @@ def test__given_auth_not_configured_and_not_required__then_dependency_allows(
assert auth_module.require_auth(token=None) is None


def test__given_auth_configured_but_not_required__then_dependency_allows(
monkeypatch,
):
monkeypatch.delenv(auth_module.GATEWAY_AUTH_DISABLED_ENV, raising=False)
monkeypatch.delenv(auth_module.GATEWAY_AUTH_REQUIRED_ENV, raising=False)
monkeypatch.setenv(auth_module.GATEWAY_AUTH_ISSUER_ENV, "https://issuer.example/")
monkeypatch.setenv(auth_module.GATEWAY_AUTH_AUDIENCE_ENV, "aud")

assert auth_module.require_auth(token=None) is None


def test__given_auth_required_and_misconfigured__then_dependency_raises_503(
monkeypatch,
):
Expand Down Expand Up @@ -169,6 +181,7 @@ def test__given_repeated_requests__then_decoder_not_reinstantiated(monkeypatch):
from policyengine_fastapi.auth import jwt_decoder as jwt_decoder_module

monkeypatch.delenv(auth_module.GATEWAY_AUTH_DISABLED_ENV, raising=False)
monkeypatch.setenv(auth_module.GATEWAY_AUTH_REQUIRED_ENV, "1")
monkeypatch.setenv(auth_module.GATEWAY_AUTH_ISSUER_ENV, "https://issuer.example/")
monkeypatch.setenv(auth_module.GATEWAY_AUTH_AUDIENCE_ENV, "aud-repeat")

Expand Down
Loading