Commit b479cbc4 authored by Sergio Gimenez's avatar Sergio Gimenez
Browse files

WIP oauth2

parent 1d810d38
Loading
Loading
Loading
Loading
+14 −3
Original line number Diff line number Diff line
@@ -8,12 +8,21 @@ from federation_manager.domain.errors import (
)

_TYPE_BASE = "urn:oop:ewbi:error:"
_BEARER_CHALLENGE = {"WWW-Authenticate": 'Bearer scope="fed-mgmt"'}


def problem(status: int, code: str, title: str, detail: str, instance: str) -> JSONResponse:
def problem(
    status: int,
    code: str,
    title: str,
    detail: str,
    instance: str,
    headers: dict[str, str] | None = None,
) -> JSONResponse:
    return JSONResponse(
        status_code=status,
        media_type="application/problem+json",
        headers=headers,
        content={
            "type": f"{_TYPE_BASE}{code}",
            "title": title,
@@ -32,8 +41,9 @@ def register_exception_handlers(app: FastAPI) -> None:
            401,
            "authentication-failed",
            "Authentication Failed",
            "Client certificate missing or unreadable.",
            "Access token missing, malformed, or lacking the required scope.",
            request.url.path,
            headers=_BEARER_CHALLENGE,
        )

    @app.exception_handler(PartnerUnknown)
@@ -42,8 +52,9 @@ def register_exception_handlers(app: FastAPI) -> None:
            401,
            "partner-unknown",
            "Unknown Partner",
            "Client certificate is not registered with this operator.",
            "Presented client identity is not registered with this operator.",
            request.url.path,
            headers=_BEARER_CHALLENGE,
        )

    @app.exception_handler(PartnerNotActive)
+4 −6
Original line number Diff line number Diff line
from typing import Annotated

from fastapi import APIRouter, Depends, Header
from fastapi import APIRouter, Depends

from federation_manager.api.security import get_bearer_token
from federation_manager.application.authentication import PartnerAuthenticator
from federation_manager.dependencies import get_partner_authenticator
from federation_manager.domain.errors import AuthenticationFailed

router = APIRouter(prefix="/ewbi/v1/management", tags=["ewbi-management"])

@@ -12,9 +12,7 @@ router = APIRouter(prefix="/ewbi/v1/management", tags=["ewbi-management"])
@router.post("/heartbeat")
async def heartbeat(
    auth: Annotated[PartnerAuthenticator, Depends(get_partner_authenticator)],
    x_client_certificate_thumbprint: Annotated[str | None, Header()] = None,
    token: Annotated[str, Depends(get_bearer_token)],
) -> dict[str, str]:
    if not x_client_certificate_thumbprint:
        raise AuthenticationFailed
    await auth.authenticate(x_client_certificate_thumbprint)
    await auth.authenticate(token)
    return {"status": "ALIVE"}
+29 −0
Original line number Diff line number Diff line
from typing import Annotated

from fastapi import Depends
from fastapi.openapi.models import OAuthFlowClientCredentials, OAuthFlows
from fastapi.security import OAuth2

from federation_manager.core.config import get_settings
from federation_manager.domain.errors import AuthenticationFailed

# securityScheme GSMA declares on the EWBI contract (OPG.04 v6.0 Sec. 9).
oauth2_client_credentials = OAuth2(
    flows=OAuthFlows(
        clientCredentials=OAuthFlowClientCredentials(
            tokenUrl=f"{get_settings().keycloak_issuer}/protocol/openid-connect/token",
            scopes={"fed-mgmt": "Access to the federation APIs"},
        )
    ),
    scheme_name="oAuth2ClientCredentials",
    auto_error=False,
)


def get_bearer_token(
    authorization: Annotated[str | None, Depends(oauth2_client_credentials)],
) -> str:
    scheme, _, token = (authorization or "").partition(" ")
    if scheme.lower() != "bearer" or not token:
        raise AuthenticationFailed
    return token
+3 −1
Original line number Diff line number Diff line
@@ -4,7 +4,9 @@ from federation_manager.domain.ports import JwtValidatorPort, PartnerRepositoryP


class PartnerAuthenticator:
    def __init__(self, partner_repo: PartnerRepositoryPort, jwt_validator: JwtValidatorPort) -> None:
    def __init__(
        self, partner_repo: PartnerRepositoryPort, jwt_validator: JwtValidatorPort
    ) -> None:
        self._partner_repo = partner_repo
        self._jwt_validator = jwt_validator

+8 −2
Original line number Diff line number Diff line
@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession

from federation_manager.adapters.database.partner_repo import PostgresPartnerRepo
from federation_manager.application.authentication import PartnerAuthenticator
from federation_manager.domain.ports import PartnerRepositoryPort
from federation_manager.domain.ports import JwtValidatorPort, PartnerRepositoryPort


async def get_session(request: Request) -> AsyncIterator[AsyncSession]:
@@ -20,7 +20,13 @@ def get_partner_repo(
    return PostgresPartnerRepo(session)


def get_jwt_validator(request: Request) -> JwtValidatorPort:
    validator: JwtValidatorPort = request.app.state.jwt_validator
    return validator


def get_partner_authenticator(
    repo: Annotated[PartnerRepositoryPort, Depends(get_partner_repo)],
    jwt_validator: Annotated[JwtValidatorPort, Depends(get_jwt_validator)],
) -> PartnerAuthenticator:
    return PartnerAuthenticator(repo)
    return PartnerAuthenticator(repo, jwt_validator)
Loading