diff --git a/poetry.lock b/poetry.lock index 3128099..53c15de 100644 --- a/poetry.lock +++ b/poetry.lock @@ -210,10 +210,9 @@ standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[stand name = "h11" version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = true +optional = false python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"test\"" files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, @@ -223,10 +222,9 @@ files = [ name = "httpcore" version = "1.0.9" description = "A minimal low-level HTTP client." -optional = true +optional = false python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"test\"" files = [ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, @@ -246,10 +244,9 @@ trio = ["trio (>=0.22.0,<1.0)"] name = "httpx" version = "0.28.1" description = "The next generation HTTP client." -optional = true +optional = false python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"test\"" files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, @@ -728,4 +725,4 @@ test = ["httpx", "pytest", "pytest-benchmark", "pytest-cov"] [metadata] lock-version = "2.1" python-versions = ">=3.8" -content-hash = "9def112c6c60f8f387c108fb503d1c7b24b0ffec9d03b8dda9c53eeb5fb7c0ec" +content-hash = "72ac204589e45ad2f4d0b56ffff92042fe2569778351ba40f1aac44193a0399b" diff --git a/pyproject.toml b/pyproject.toml index 68ee418..04e0c69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ dependencies = [ "pydantic", "fastapi", "sentry-sdk[fastapi]", + "httpx (>=0.28.1,<0.29.0)", ] [project.optional-dependencies] diff --git a/vp_core/clients/__init__.py b/vp_core/clients/__init__.py new file mode 100644 index 0000000..0ec78e9 --- /dev/null +++ b/vp_core/clients/__init__.py @@ -0,0 +1,7 @@ +""" +HTTP client utilities for interacting with external/internal services. +""" + +from .volopay_be import validate_token + +__all__ = ["validate_token"] diff --git a/vp_core/clients/volopay_be.py b/vp_core/clients/volopay_be.py new file mode 100644 index 0000000..abc0fb9 --- /dev/null +++ b/vp_core/clients/volopay_be.py @@ -0,0 +1,112 @@ +""" +Volopay Backend (volo-be) HTTP client utilities. + +This module provides shared utilities for microservices that need to interact +with the main volo-be backend, primarily for auth token validation. + +Usage: + from vp_core.clients.volopay_be import validate_token + + valid = await validate_token( + client="web", + access_token="abc123", + uid="user@example.com", + account="volopay", + volo_be_url="https://api.volopay.com" + ) +""" + +import httpx + + +async def validate_token( + client: str, + access_token: str, + uid: str, + account: str, + volo_be_url: str, + x_feature: str | None = None, + env: str | None = None, +) -> bool: + """ + Validates a user's session token by calling volo-be's token validation endpoint. + + This is used by satellite microservices (volo-agents, ocr-reader, etc.) to verify + that a frontend user's token is valid without maintaining their own user database. + + Args: + client: Client identifier (e.g., "web", "mobile") + access_token: User's session access token + uid: User identifier (typically email) + account: Account/organization identifier + volo_be_url: Base URL of volo-be (e.g., "https://api.volopay.com") + x_feature: Optional feature flag header + env: Environment name (defaults to None). If "test", always returns True. + + Returns: + True if the token is valid (volo-be returned 200), False otherwise. + + Example: + >>> valid = await validate_token( + ... client="web", + ... access_token="eyJ0eXAi...", + ... uid="user@example.com", + ... account="volopay", + ... volo_be_url="https://api.volopay.com" + ... ) + >>> if valid: + ... # proceed with authenticated request + """ + # Skip validation in test environments + if env == "test": + return True + + headers = { + "client": client, + "access-token": access_token, + "uid": uid, + "account": account, + } + + # Add optional x_feature header if provided + if x_feature: + headers["x_feature"] = x_feature + + async with httpx.AsyncClient(base_url=volo_be_url, headers=headers) as http: + try: + response = await http.get("/api/v3/auth/user/validate_token") + return response.status_code == 200 + except httpx.HTTPError: + # Network errors, timeouts, etc. — treat as invalid token + return False + + +# Future: Two-layer auth builder +# When multiple microservices (volo-agents, ocr-reader, future services) all adopt +# the same two-layer auth pattern (shared secret + token validation fallback), +# consider adding a generic FastAPI dependency builder here: +# +# def create_two_layer_auth_dependency( +# service_name: str, # "agents", "ocr", etc. +# secret_env_var: str, # "VOLO_BE_AGENTS_SECRET" +# volo_be_url_env_var: str = "VOLO_BE_URL", +# env_var: str = "ENV" +# ) -> Callable: +# """ +# Creates a FastAPI Depends() function that enforces two-layer auth: +# 1. Checks volo_be_{service_name}_secret header against env var +# 2. Falls back to validate_token() if Layer 1 fails +# +# Returns: +# A FastAPI dependency (Depends-compatible callable) +# +# Example: +# from vp_core.clients import create_two_layer_auth_dependency +# +# AuthDep = create_two_layer_auth_dependency( +# service_name="agents", +# secret_env_var="VOLO_BE_AGENTS_SECRET" +# ) +# +# app.include_router(router, dependencies=[AuthDep]) +# """