mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-10-04 23:02:01 +00:00
fix(auth): distinguish portal verification outages
This commit is contained in:
parent
387bf64b6b
commit
28ec13584a
6 changed files with 66 additions and 15 deletions
|
|
@ -18,7 +18,11 @@ import urllib.error
|
|||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
from shared.cognito import looks_like_cognito_token, verify_cognito_id_token
|
||||
from shared.cognito import (
|
||||
CognitoVerificationUnavailable,
|
||||
looks_like_cognito_token,
|
||||
verify_cognito_id_token,
|
||||
)
|
||||
from shared.db import get_settings
|
||||
from shared.secrets import get_parameter
|
||||
|
||||
|
|
@ -101,7 +105,11 @@ def lambda_handler(event, context):
|
|||
return _DENY
|
||||
|
||||
if looks_like_cognito_token(token):
|
||||
user_info = _verify_portal_token(token)
|
||||
try:
|
||||
user_info = _verify_portal_token(token)
|
||||
except CognitoVerificationUnavailable:
|
||||
logger.error("Cognito verification service unavailable; denying")
|
||||
return _DENY
|
||||
else:
|
||||
if not os.environ.get("GOOGLE_CLIENT_ID_PARAM", ""):
|
||||
logger.error("Authorizer misconfigured: GOOGLE_CLIENT_ID_PARAM unset")
|
||||
|
|
|
|||
|
|
@ -13,7 +13,11 @@ from zoneinfo import ZoneInfo
|
|||
|
||||
import boto3
|
||||
|
||||
from shared.cognito import looks_like_cognito_token, verify_cognito_id_token
|
||||
from shared.cognito import (
|
||||
CognitoVerificationUnavailable,
|
||||
looks_like_cognito_token,
|
||||
verify_cognito_id_token,
|
||||
)
|
||||
from shared.db import (
|
||||
current_week,
|
||||
delete_order,
|
||||
|
|
@ -180,7 +184,10 @@ def _verify_admin(event) -> tuple[dict | None, dict | None]:
|
|||
return None, response(403, {"error": "Authentication required"})
|
||||
|
||||
if looks_like_cognito_token(token):
|
||||
user_info = _verify_portal_token(token)
|
||||
try:
|
||||
user_info = _verify_portal_token(token)
|
||||
except CognitoVerificationUnavailable:
|
||||
return None, _authentication_service_unavailable()
|
||||
if user_info is None:
|
||||
return None, response(
|
||||
403, {"error": "Invalid or unauthorized portal account"}
|
||||
|
|
@ -541,7 +548,10 @@ def handle_submit(event):
|
|||
|
||||
portal_token = _extract_bearer_token(event)
|
||||
if portal_token:
|
||||
user_info = _verify_portal_token(portal_token)
|
||||
try:
|
||||
user_info = _verify_portal_token(portal_token)
|
||||
except CognitoVerificationUnavailable:
|
||||
return _authentication_service_unavailable()
|
||||
if user_info is None:
|
||||
return response(403, {"error": "Invalid or unauthorized portal account"})
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from functools import lru_cache
|
|||
|
||||
import jwt
|
||||
from jwt import PyJWKClient
|
||||
from jwt.exceptions import PyJWTError
|
||||
from jwt.exceptions import PyJWKClientConnectionError, PyJWKClientError, PyJWTError
|
||||
|
||||
from shared.secrets import get_parameter
|
||||
|
||||
|
|
@ -18,6 +18,10 @@ _COGNITO_ISSUER_RE = re.compile(
|
|||
)
|
||||
|
||||
|
||||
class CognitoVerificationUnavailable(RuntimeError):
|
||||
"""Trusted Cognito configuration or JWKS could not be loaded."""
|
||||
|
||||
|
||||
def looks_like_cognito_token(token: str) -> bool:
|
||||
"""Return whether untrusted claims identify a Cognito ID token."""
|
||||
try:
|
||||
|
|
@ -62,11 +66,11 @@ def verify_cognito_id_token(
|
|||
audience = get_parameter(audience_param, decrypt=False)
|
||||
except Exception as exc:
|
||||
logger.error("Failed to load Cognito verification parameters: %s", exc)
|
||||
return None
|
||||
raise CognitoVerificationUnavailable from exc
|
||||
|
||||
if not _COGNITO_ISSUER_RE.fullmatch(issuer) or not audience:
|
||||
logger.error("Cognito verification parameters are invalid")
|
||||
return None
|
||||
raise CognitoVerificationUnavailable
|
||||
|
||||
try:
|
||||
signing_key = _jwk_client(issuer).get_signing_key_from_jwt(token)
|
||||
|
|
@ -89,7 +93,13 @@ def verify_cognito_id_token(
|
|||
]
|
||||
},
|
||||
)
|
||||
except Exception as exc:
|
||||
except PyJWKClientConnectionError as exc:
|
||||
logger.error("Cognito JWKS is unavailable: %s", type(exc).__name__)
|
||||
raise CognitoVerificationUnavailable from exc
|
||||
except OSError as exc:
|
||||
logger.error("Cognito JWKS is unavailable: %s", type(exc).__name__)
|
||||
raise CognitoVerificationUnavailable from exc
|
||||
except (PyJWKClientError, PyJWTError, TypeError, ValueError) as exc:
|
||||
logger.warning("Cognito token rejected: %s", type(exc).__name__)
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -102,6 +102,17 @@ def test_invalid_portal_token_denied_without_google_fallback(
|
|||
mock_google.assert_not_called()
|
||||
|
||||
|
||||
@patch(
|
||||
"admin_authorizer_handler._verify_portal_token",
|
||||
side_effect=authorizer.CognitoVerificationUnavailable,
|
||||
)
|
||||
@patch("admin_authorizer_handler.looks_like_cognito_token", return_value=True)
|
||||
def test_portal_verification_outage_denied(mock_looks_like, mock_verify):
|
||||
assert authorizer.lambda_handler(_event("portal-id-token"), None) == {
|
||||
"isAuthorized": False
|
||||
}
|
||||
|
||||
|
||||
@patch.dict(
|
||||
os.environ, {"GOOGLE_CLIENT_ID_PARAM": "/meal-order-manager/google-client-id"}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -107,12 +107,10 @@ def test_ssm_failure_is_rejected(signing_key):
|
|||
with patch(
|
||||
"shared.cognito.get_parameter", side_effect=RuntimeError("SSM unavailable")
|
||||
):
|
||||
assert (
|
||||
with pytest.raises(cognito.CognitoVerificationUnavailable):
|
||||
cognito.verify_cognito_id_token(
|
||||
_token(private_key), ISSUER_PARAM, AUDIENCE_PARAM
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_jwks_failure_is_rejected(signing_key):
|
||||
|
|
@ -123,12 +121,10 @@ def test_jwks_failure_is_rejected(signing_key):
|
|||
patch("shared.cognito.get_parameter", side_effect=_parameter),
|
||||
patch("shared.cognito._jwk_client", return_value=jwks),
|
||||
):
|
||||
assert (
|
||||
with pytest.raises(cognito.CognitoVerificationUnavailable):
|
||||
cognito.verify_cognito_id_token(
|
||||
_token(private_key), ISSUER_PARAM, AUDIENCE_PARAM
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_cognito_token_detection_is_untrusted_routing_hint(signing_key):
|
||||
|
|
|
|||
|
|
@ -680,6 +680,22 @@ def test_invalid_portal_token_never_falls_back_to_google_or_body(
|
|||
mock_put.assert_not_called()
|
||||
|
||||
|
||||
@patch("submit_order_handler.put_order")
|
||||
@patch(
|
||||
"submit_order_handler._verify_portal_token",
|
||||
side_effect=submit_order_handler.CognitoVerificationUnavailable,
|
||||
)
|
||||
def test_portal_verification_outage_returns_503(mock_portal, mock_put):
|
||||
event = _submit_event(_make_items([(10.00, 1)]))
|
||||
event["headers"] = {"authorization": "Bearer portal-id-token"}
|
||||
|
||||
status, body = _parse_response(submit_order_handler.lambda_handler(event, None))
|
||||
|
||||
assert status == 503
|
||||
assert body == {"error": "Authentication service temporarily unavailable"}
|
||||
mock_put.assert_not_called()
|
||||
|
||||
|
||||
@patch(
|
||||
"submit_order_handler.get_settings",
|
||||
return_value={"admin_emails": ["portal.admin@seahaven.com"]},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue