diff --git a/README.md b/README.md index 717d3ba..b3eab89 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,14 @@ The application is configurable via environment variables. - **Type:** JSON object - **Required:** No, defaults to `null` (disabled) - **Example:** `{"type": "http", "scheme": "bearer", "bearerFormat": "JWT", "description": "Paste your raw JWT here. This API uses Bearer token authorization.\n"}` + - **`SWAGGER_UI_ENDPOINT`**, path of Swagger UI, used for augmenting spec response with auth configuration + - **Type:** string or null + - **Required:** No, defaults to `/api.html` + - **Example:** `/api` + - **`SWAGGER_UI_INIT_OAUTH`**, initialization options for the [Swagger UI OAuth2 configuration](https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/) + - **Type:** JSON object + - **Required:** No, defaults to `null` (disabled) + - **Example:** `{"clientId": "stac-auth-proxy", "usePkceWithAuthorizationCodeGrant": true}` - Filtering - **`ITEMS_FILTER_CLS`**, CQL2 expression generator for item-level filtering - **Type:** JSON object with class configuration diff --git a/src/stac_auth_proxy/app.py b/src/stac_auth_proxy/app.py index 82264b6..0682f68 100644 --- a/src/stac_auth_proxy/app.py +++ b/src/stac_auth_proxy/app.py @@ -13,7 +13,7 @@ from starlette_cramjam.middleware import CompressionMiddleware from .config import Settings -from .handlers import HealthzHandler, ReverseProxyHandler +from .handlers import HealthzHandler, ReverseProxyHandler, SwaggerUI from .middleware import ( AddProcessTimeHeaderMiddleware, ApplyCql2FilterMiddleware, @@ -78,6 +78,18 @@ async def lifespan(app: FastAPI): # Handlers (place catch-all proxy handler last) # + if settings.swagger_ui_endpoint: + assert ( + settings.openapi_spec_endpoint + ), "openapi_spec_endpoint must be set when using swagger_ui_endpoint" + app.add_route( + settings.swagger_ui_endpoint, + SwaggerUI( + openapi_url=settings.openapi_spec_endpoint, + init_oauth=settings.swagger_ui_init_oauth, + ).route, + include_in_schema=False, + ) if settings.healthz_prefix: app.include_router( HealthzHandler(upstream_url=str(settings.upstream_url)).router, diff --git a/src/stac_auth_proxy/config.py b/src/stac_auth_proxy/config.py index ce6cf54..c2ca946 100644 --- a/src/stac_auth_proxy/config.py +++ b/src/stac_auth_proxy/config.py @@ -45,9 +45,12 @@ class Settings(BaseSettings): check_conformance: bool = True enable_compression: bool = True + # OpenAPI / Swagger UI openapi_spec_endpoint: Optional[str] = Field(pattern=_PREFIX_PATTERN, default=None) openapi_auth_scheme_name: str = "oidcAuth" openapi_auth_scheme_override: Optional[dict] = None + swagger_ui_endpoint: Optional[str] = None + swagger_ui_init_oauth: dict = Field(default_factory=dict) # Auth enable_authentication_extension: bool = True diff --git a/src/stac_auth_proxy/handlers/__init__.py b/src/stac_auth_proxy/handlers/__init__.py index 52c6c97..b0b6cf0 100644 --- a/src/stac_auth_proxy/handlers/__init__.py +++ b/src/stac_auth_proxy/handlers/__init__.py @@ -2,5 +2,6 @@ from .healthz import HealthzHandler from .reverse_proxy import ReverseProxyHandler +from .swagger_ui import SwaggerUI -__all__ = ["ReverseProxyHandler", "HealthzHandler"] +__all__ = ["ReverseProxyHandler", "HealthzHandler", "SwaggerUI"] diff --git a/src/stac_auth_proxy/handlers/swagger_ui.py b/src/stac_auth_proxy/handlers/swagger_ui.py new file mode 100644 index 0000000..1b885a2 --- /dev/null +++ b/src/stac_auth_proxy/handlers/swagger_ui.py @@ -0,0 +1,41 @@ +""" +In order to allow customization fo the Swagger UI's OAuth2 configuration, we support +overriding the default handler. This is useful for adding custom parameters such as +`usePkceWithAuthorizationCodeGrant` or `clientId`. + +See: +- https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/ +""" + +from dataclasses import dataclass, field +from typing import Optional + +from fastapi.openapi.docs import get_swagger_ui_html +from starlette.requests import Request +from starlette.responses import HTMLResponse + + +@dataclass +class SwaggerUI: + """Swagger UI handler.""" + + openapi_url: str + title: Optional[str] = "STAC API" + init_oauth: dict = field(default_factory=dict) + parameters: dict = field(default_factory=dict) + oauth2_redirect_url: str = "/docs/oauth2-redirect" + + async def route(self, req: Request) -> HTMLResponse: + """Route handler.""" + root_path = req.scope.get("root_path", "").rstrip("/") + openapi_url = root_path + self.openapi_url + oauth2_redirect_url = self.oauth2_redirect_url + if oauth2_redirect_url: + oauth2_redirect_url = root_path + oauth2_redirect_url + return get_swagger_ui_html( + openapi_url=openapi_url, + title=f"{self.title} - Swagger UI", + oauth2_redirect_url=oauth2_redirect_url, + init_oauth=self.init_oauth, + swagger_ui_parameters=self.parameters, + )