Loading src/open_exposure_gateway/adapters/http/keycloak_token_verifier.py 0 → 100644 +24 −0 Original line number Diff line number Diff line from open_exposure_gateway.core.auth.jwks_cache import JWKSCache from open_exposure_gateway.core.auth.tokens import DecodedClaims, validate_token from open_exposure_gateway.core.config import KeycloakSettings class KeycloakTokenVerifier: """Adapter implementing TokenVerifierPort against Keycloak: the JWT verification logic itself lives in core/auth (JWKSCache + validate_token), this just gives callers a swappable seam -- a future auth mechanism only needs a new adapter here, not changes to dependencies.py/health.py.""" def __init__(self, settings: KeycloakSettings) -> None: self._settings = settings self._jwks_cache = JWKSCache(settings.jwks_url) @property def is_healthy(self) -> bool: return self._jwks_cache.is_healthy async def warm(self) -> None: await self._jwks_cache.warm() async def verify(self, token: str) -> DecodedClaims: return await validate_token(token, self._jwks_cache, self._settings) src/open_exposure_gateway/api/platform/health.py +8 −4 Original line number Diff line number Diff line Loading @@ -2,10 +2,14 @@ from typing import Annotated from fastapi import APIRouter, Depends, Response, status from open_exposure_gateway.core.auth.jwks_cache import JWKSCache from open_exposure_gateway.core.config import get_settings from open_exposure_gateway.dependencies import get_database_health, get_jwks_cache, get_publisher from open_exposure_gateway.dependencies import ( get_database_health, get_publisher, get_token_verifier, ) from open_exposure_gateway.ports.databus_port import DataBusPort from open_exposure_gateway.ports.token_verifier_port import TokenVerifierPort from open_exposure_gateway.schemas.common import HealthResponse, HealthStatus router = APIRouter(tags=["Platform"]) Loading @@ -26,10 +30,10 @@ async def readyz( response: Response, publisher: Annotated[DataBusPort, Depends(get_publisher)], database_is_healthy: Annotated[bool, Depends(get_database_health)], jwks_cache: Annotated[JWKSCache, Depends(get_jwks_cache)], token_verifier: Annotated[TokenVerifierPort, Depends(get_token_verifier)], ) -> HealthResponse: settings = get_settings() if not publisher.is_connected or not database_is_healthy or not jwks_cache.is_healthy: if not publisher.is_connected or not database_is_healthy or not token_verifier.is_healthy: response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE return HealthResponse( status=HealthStatus.NOT_OK, Loading src/open_exposure_gateway/core/state.py +2 −2 Original line number Diff line number Diff line Loading @@ -2,10 +2,10 @@ from typing import Protocol from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker from open_exposure_gateway.core.auth.jwks_cache import JWKSCache from open_exposure_gateway.ports.databus_port import DataBusPort from open_exposure_gateway.ports.qod_callback_port import QodCallbackDeliveryPort from open_exposure_gateway.ports.srm_port import SRMClientPort from open_exposure_gateway.ports.token_verifier_port import TokenVerifierPort class AppState(Protocol): Loading @@ -14,4 +14,4 @@ class AppState(Protocol): db_engine: AsyncEngine session_maker: async_sessionmaker[AsyncSession] qod_callback_client: QodCallbackDeliveryPort jwks_cache: JWKSCache token_verifier: TokenVerifierPort src/open_exposure_gateway/dependencies.py +6 −7 Original line number Diff line number Diff line Loading @@ -39,8 +39,7 @@ from open_exposure_gateway.application.services.location_retrieval_service impor from open_exposure_gateway.application.services.quality_on_demand_service import ( QualityOnDemandService, ) from open_exposure_gateway.core.auth.jwks_cache import JWKSCache from open_exposure_gateway.core.auth.tokens import TokenFlow, validate_token from open_exposure_gateway.core.auth.tokens import TokenFlow from open_exposure_gateway.core.config import get_settings from open_exposure_gateway.core.exceptions import UnauthorizedException from open_exposure_gateway.core.state import AppState Loading @@ -56,6 +55,7 @@ from open_exposure_gateway.ports.database.registration import AppRegistrationRep from open_exposure_gateway.ports.databus_port import DataBusPort from open_exposure_gateway.ports.qod_callback_port import QodCallbackDeliveryPort from open_exposure_gateway.ports.srm_port import SRMClientPort from open_exposure_gateway.ports.token_verifier_port import TokenVerifierPort @dataclass Loading Loading @@ -83,8 +83,8 @@ def get_publisher(request: Request) -> DataBusPort: return get_app_state(request=request).publisher def get_jwks_cache(request: Request) -> JWKSCache: return get_app_state(request=request).jwks_cache def get_token_verifier(request: Request) -> TokenVerifierPort: return get_app_state(request=request).token_verifier _bearer_scheme = HTTPBearer(auto_error=False) Loading @@ -94,13 +94,12 @@ async def get_caller_context( request: Request, x_correlator: XCorrelatorHeader = None, credentials: Optional[HTTPAuthorizationCredentials] = Depends(_bearer_scheme), token_verifier: TokenVerifierPort = Depends(get_token_verifier), ) -> CallerContext: if credentials is None: raise UnauthorizedException(message="Missing bearer token") jwks_cache = get_app_state(request=request).jwks_cache keycloak_settings = get_settings().keycloak_settings decoded = await validate_token(credentials.credentials, jwks_cache, keycloak_settings) decoded = await token_verifier.verify(credentials.credentials) claims = decoded.claims tenant_id = claims.get("organization") Loading src/open_exposure_gateway/main.py +5 −5 Original line number Diff line number Diff line Loading @@ -32,6 +32,7 @@ from open_exposure_gateway.adapters.databus.nats_adapter import ( NatsOperationStatusConsumer, ) from open_exposure_gateway.adapters.http.callback_client import HttpCallbackClient from open_exposure_gateway.adapters.http.keycloak_token_verifier import KeycloakTokenVerifier from open_exposure_gateway.adapters.http.qod_callback_client import HttpQodCallbackClient from open_exposure_gateway.adapters.http.srm_client import SRMClient from open_exposure_gateway.api.camara.edge_application_management.v0_1_0_alpha_1.router import ( Loading @@ -54,7 +55,6 @@ from open_exposure_gateway.application.services.edge_application_management_serv from open_exposure_gateway.application.services.quality_on_demand_service import ( QualityOnDemandService, ) from open_exposure_gateway.core.auth.jwks_cache import JWKSCache from open_exposure_gateway.core.config import get_settings from open_exposure_gateway.core.logging import configure_logging from open_exposure_gateway.domain.edge_application_management import ( Loading Loading @@ -317,9 +317,9 @@ async def default_lifespan(app: FastAPI) -> AsyncGenerator[None, None]: qod_callback_client = HttpQodCallbackClient() jwks_cache = JWKSCache(settings.keycloak_settings.jwks_url) await jwks_cache.warm() if jwks_cache.is_healthy: token_verifier = KeycloakTokenVerifier(settings.keycloak_settings) await token_verifier.warm() if token_verifier.is_healthy: logger.info("Keycloak JWKS cache warmed") else: # Not fatal: /readyz surfaces this (architecture.md E3) so the pod is Loading Loading @@ -392,7 +392,7 @@ async def default_lifespan(app: FastAPI) -> AsyncGenerator[None, None]: app.state.db_engine = db_engine app.state.session_maker = session_maker app.state.qod_callback_client = qod_callback_client app.state.jwks_cache = jwks_cache app.state.token_verifier = token_verifier yield Loading Loading
src/open_exposure_gateway/adapters/http/keycloak_token_verifier.py 0 → 100644 +24 −0 Original line number Diff line number Diff line from open_exposure_gateway.core.auth.jwks_cache import JWKSCache from open_exposure_gateway.core.auth.tokens import DecodedClaims, validate_token from open_exposure_gateway.core.config import KeycloakSettings class KeycloakTokenVerifier: """Adapter implementing TokenVerifierPort against Keycloak: the JWT verification logic itself lives in core/auth (JWKSCache + validate_token), this just gives callers a swappable seam -- a future auth mechanism only needs a new adapter here, not changes to dependencies.py/health.py.""" def __init__(self, settings: KeycloakSettings) -> None: self._settings = settings self._jwks_cache = JWKSCache(settings.jwks_url) @property def is_healthy(self) -> bool: return self._jwks_cache.is_healthy async def warm(self) -> None: await self._jwks_cache.warm() async def verify(self, token: str) -> DecodedClaims: return await validate_token(token, self._jwks_cache, self._settings)
src/open_exposure_gateway/api/platform/health.py +8 −4 Original line number Diff line number Diff line Loading @@ -2,10 +2,14 @@ from typing import Annotated from fastapi import APIRouter, Depends, Response, status from open_exposure_gateway.core.auth.jwks_cache import JWKSCache from open_exposure_gateway.core.config import get_settings from open_exposure_gateway.dependencies import get_database_health, get_jwks_cache, get_publisher from open_exposure_gateway.dependencies import ( get_database_health, get_publisher, get_token_verifier, ) from open_exposure_gateway.ports.databus_port import DataBusPort from open_exposure_gateway.ports.token_verifier_port import TokenVerifierPort from open_exposure_gateway.schemas.common import HealthResponse, HealthStatus router = APIRouter(tags=["Platform"]) Loading @@ -26,10 +30,10 @@ async def readyz( response: Response, publisher: Annotated[DataBusPort, Depends(get_publisher)], database_is_healthy: Annotated[bool, Depends(get_database_health)], jwks_cache: Annotated[JWKSCache, Depends(get_jwks_cache)], token_verifier: Annotated[TokenVerifierPort, Depends(get_token_verifier)], ) -> HealthResponse: settings = get_settings() if not publisher.is_connected or not database_is_healthy or not jwks_cache.is_healthy: if not publisher.is_connected or not database_is_healthy or not token_verifier.is_healthy: response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE return HealthResponse( status=HealthStatus.NOT_OK, Loading
src/open_exposure_gateway/core/state.py +2 −2 Original line number Diff line number Diff line Loading @@ -2,10 +2,10 @@ from typing import Protocol from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker from open_exposure_gateway.core.auth.jwks_cache import JWKSCache from open_exposure_gateway.ports.databus_port import DataBusPort from open_exposure_gateway.ports.qod_callback_port import QodCallbackDeliveryPort from open_exposure_gateway.ports.srm_port import SRMClientPort from open_exposure_gateway.ports.token_verifier_port import TokenVerifierPort class AppState(Protocol): Loading @@ -14,4 +14,4 @@ class AppState(Protocol): db_engine: AsyncEngine session_maker: async_sessionmaker[AsyncSession] qod_callback_client: QodCallbackDeliveryPort jwks_cache: JWKSCache token_verifier: TokenVerifierPort
src/open_exposure_gateway/dependencies.py +6 −7 Original line number Diff line number Diff line Loading @@ -39,8 +39,7 @@ from open_exposure_gateway.application.services.location_retrieval_service impor from open_exposure_gateway.application.services.quality_on_demand_service import ( QualityOnDemandService, ) from open_exposure_gateway.core.auth.jwks_cache import JWKSCache from open_exposure_gateway.core.auth.tokens import TokenFlow, validate_token from open_exposure_gateway.core.auth.tokens import TokenFlow from open_exposure_gateway.core.config import get_settings from open_exposure_gateway.core.exceptions import UnauthorizedException from open_exposure_gateway.core.state import AppState Loading @@ -56,6 +55,7 @@ from open_exposure_gateway.ports.database.registration import AppRegistrationRep from open_exposure_gateway.ports.databus_port import DataBusPort from open_exposure_gateway.ports.qod_callback_port import QodCallbackDeliveryPort from open_exposure_gateway.ports.srm_port import SRMClientPort from open_exposure_gateway.ports.token_verifier_port import TokenVerifierPort @dataclass Loading Loading @@ -83,8 +83,8 @@ def get_publisher(request: Request) -> DataBusPort: return get_app_state(request=request).publisher def get_jwks_cache(request: Request) -> JWKSCache: return get_app_state(request=request).jwks_cache def get_token_verifier(request: Request) -> TokenVerifierPort: return get_app_state(request=request).token_verifier _bearer_scheme = HTTPBearer(auto_error=False) Loading @@ -94,13 +94,12 @@ async def get_caller_context( request: Request, x_correlator: XCorrelatorHeader = None, credentials: Optional[HTTPAuthorizationCredentials] = Depends(_bearer_scheme), token_verifier: TokenVerifierPort = Depends(get_token_verifier), ) -> CallerContext: if credentials is None: raise UnauthorizedException(message="Missing bearer token") jwks_cache = get_app_state(request=request).jwks_cache keycloak_settings = get_settings().keycloak_settings decoded = await validate_token(credentials.credentials, jwks_cache, keycloak_settings) decoded = await token_verifier.verify(credentials.credentials) claims = decoded.claims tenant_id = claims.get("organization") Loading
src/open_exposure_gateway/main.py +5 −5 Original line number Diff line number Diff line Loading @@ -32,6 +32,7 @@ from open_exposure_gateway.adapters.databus.nats_adapter import ( NatsOperationStatusConsumer, ) from open_exposure_gateway.adapters.http.callback_client import HttpCallbackClient from open_exposure_gateway.adapters.http.keycloak_token_verifier import KeycloakTokenVerifier from open_exposure_gateway.adapters.http.qod_callback_client import HttpQodCallbackClient from open_exposure_gateway.adapters.http.srm_client import SRMClient from open_exposure_gateway.api.camara.edge_application_management.v0_1_0_alpha_1.router import ( Loading @@ -54,7 +55,6 @@ from open_exposure_gateway.application.services.edge_application_management_serv from open_exposure_gateway.application.services.quality_on_demand_service import ( QualityOnDemandService, ) from open_exposure_gateway.core.auth.jwks_cache import JWKSCache from open_exposure_gateway.core.config import get_settings from open_exposure_gateway.core.logging import configure_logging from open_exposure_gateway.domain.edge_application_management import ( Loading Loading @@ -317,9 +317,9 @@ async def default_lifespan(app: FastAPI) -> AsyncGenerator[None, None]: qod_callback_client = HttpQodCallbackClient() jwks_cache = JWKSCache(settings.keycloak_settings.jwks_url) await jwks_cache.warm() if jwks_cache.is_healthy: token_verifier = KeycloakTokenVerifier(settings.keycloak_settings) await token_verifier.warm() if token_verifier.is_healthy: logger.info("Keycloak JWKS cache warmed") else: # Not fatal: /readyz surfaces this (architecture.md E3) so the pod is Loading Loading @@ -392,7 +392,7 @@ async def default_lifespan(app: FastAPI) -> AsyncGenerator[None, None]: app.state.db_engine = db_engine app.state.session_maker = session_maker app.state.qod_callback_client = qod_callback_client app.state.jwks_cache = jwks_cache app.state.token_verifier = token_verifier yield Loading