Commit 3c615e0d authored by George Papathanail's avatar George Papathanail
Browse files

refactor: reintroduce TokenVerifierPort/KeycloakTokenVerifier over core/auth

parent c11173bf
Loading
Loading
Loading
Loading
Loading
+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)
+8 −4
Original line number Diff line number Diff line
@@ -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"])
@@ -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,
+2 −2
Original line number Diff line number Diff line
@@ -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):
@@ -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
+6 −7
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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)
@@ -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")
+5 −5
Original line number Diff line number Diff line
@@ -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 (
@@ -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 (
@@ -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
@@ -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