Name | stac-auth-proxy JSON |
Version |
0.7.0
JSON |
| download |
home_page | None |
Summary | STAC authentication proxy with FastAPI |
upload_time | 2025-07-22 17:19:13 |
maintainer | None |
docs_url | None |
author | None |
requires_python | >=3.9 |
license | MIT License Copyright (c) 2024 Development Seed Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
keywords |
authentication
fastapi
proxy
stac
|
VCS |
|
bugtrack_url |
|
requirements |
No requirements were recorded.
|
Travis-CI |
No Travis.
|
coveralls test coverage |
No coveralls.
|
<div align="center">
<h1 style="font-family: monospace">stac auth proxy</h1>
<p align="center">Reverse proxy to apply auth*n to your STAC API.</p>
</div>
---
STAC Auth Proxy is a proxy API that mediates between the client and your internally accessible STAC API to provide flexible authentication, authorization, and content-filtering mechanisms.
> [!IMPORTANT]
> **We would :heart: to hear from you!**
> Please [join the discussion](https://github.com/developmentseed/eoAPI/discussions/209) and let us know how you're using eoAPI! This helps us improve the project for you and others.
> If you prefer to remain anonymous, you can email us at eoapi@developmentseed.org, and we'll be happy to post a summary on your behalf.
## β¨Featuresβ¨
- **π Authentication:** Apply [OpenID Connect (OIDC)](https://openid.net/developers/how-connect-works/) token validation and optional scope checks to specified endpoints and methods
- **π Content Filtering:** Use CQL2 filters via the [Filter Extension](https://github.com/stac-api-extensions/filter?tab=readme-ov-file) to tailor API responses based on request context (e.g. user role)
- **π€ External Policy Integration:** Integrate with external systems (e.g. [Open Policy Agent (OPA)](https://www.openpolicyagent.org/)) to generate CQL2 filters dynamically from policy decisions
- **π§© Authentication Extension:** Add the [Authentication Extension](https://github.com/stac-extensions/authentication) to API responses to expose auth-related metadata
- **π OpenAPI Augmentation:** Enhance the [OpenAPI spec](https://swagger.io/specification/) with security details to keep auto-generated docs and UIs (e.g., [Swagger UI](https://swagger.io/tools/swagger-ui/)) accurate
- **ποΈ Response Compression:** Optimize response sizes using [`starlette-cramjam`](https://github.com/developmentseed/starlette-cramjam/)
## Usage
### Running
The simplest way to run the project is by invoking the application via Docker:
```sh
docker run \
-it --rm \
-p 8000:8000 \
-e UPSTREAM_URL=https://my-stac-api \
-e OIDC_DISCOVERY_URL=https://my-auth-server/.well-known/openid-configuration \
ghcr.io/developmentseed/stac-auth-proxy:latest
```
Alternatively, the module can be invoked directly or the application's factory can be passed to Uvicorn:
```sh
python -m stac_auth_proxy
```
```sh
uvicorn --factory stac_auth_proxy:create_app
```
### Installation
For local development, we use [`uv`](https://docs.astral.sh/uv/) to manage project dependencies and environment.
```sh
uv sync
```
Otherwise, the application can be installed as a standard Python module:
```sh
pip install -e .
```
> [!NOTE]
> This project will be available on PyPi in the near future[^30].
### Configuration
The application is configurable via environment variables.
#### Core
- **`UPSTREAM_URL`**, STAC API URL
- **Type:** HTTP(S) URL
- **Required:** Yes
- **Example:** `https://your-stac-api.com/stac`
- **`WAIT_FOR_UPSTREAM`**, wait for upstream API to become available before starting proxy
- **Type:** boolean
- **Required:** No, defaults to `true`
- **Example:** `false`, `1`, `True`
- **`CHECK_CONFORMANCE`**, ensure upstream API conforms to required conformance classes before starting proxy
- **Type:** boolean
- **Required:** No, defaults to `true`
- **Example:** `false`, `1`, `True`
- **`ENABLE_COMPRESSION`**, enable response compression
- **Type:** boolean
- **Required:** No, defaults to `true`
- **Example:** `false`, `1`, `True`
- **`HEALTHZ_PREFIX`**, path prefix for health check endpoints
- **Type:** string
- **Required:** No, defaults to `/healthz`
- **Example:** `''` (disabled)
- **`OVERRIDE_HOST`**, override the host header for the upstream API
- **Type:** boolean
- **Required:** No, defaults to `true`
- **Example:** `false`, `1`, `True`
- **`ROOT_PATH`**, path prefix for the proxy API
- **Type:** string
- **Required:** No, defaults to `''` (root path)
- **Example:** `/api/v1`
- **Note:** This is independent of the upstream API's path. The proxy will handle removing this prefix from incoming requests and adding it to outgoing links.
#### Authentication
- **`OIDC_DISCOVERY_URL`**, OpenID Connect discovery document URL
- **Type:** HTTP(S) URL
- **Required:** Yes
- **Example:** `https://auth.example.com/.well-known/openid-configuration`
- **`OIDC_DISCOVERY_INTERNAL_URL`**, internal network OpenID Connect discovery document URL
- **Type:** HTTP(S) URL
- **Required:** No, defaults to the value of `OIDC_DISCOVERY_URL`
- **Example:** `http://auth/.well-known/openid-configuration`
- **`DEFAULT_PUBLIC`**, default access policy for endpoints
- **Type:** boolean
- **Required:** No, defaults to `false`
- **Example:** `false`, `1`, `True`
- **`PRIVATE_ENDPOINTS`**, endpoints explicitly marked as requiring authentication and possibly scopes
- **Type:** JSON object mapping regex patterns to HTTP methods OR tuples of an HTTP method and string representing required scopes
- **Required:** No, defaults to the following:
```json
{
"^/collections$": ["POST"],
"^/collections/([^/]+)$": ["PUT", "PATCH", "DELETE"],
"^/collections/([^/]+)/items$": ["POST"],
"^/collections/([^/]+)/items/([^/]+)$": ["PUT", "PATCH", "DELETE"],
"^/collections/([^/]+)/bulk_items$": ["POST"]
}
```
- **`PUBLIC_ENDPOINTS`**, endpoints explicitly marked as not requiring authentication, used when `DEFAULT_PUBLIC == False`
- **Type:** JSON object mapping regex patterns to HTTP methods
- **Required:** No, defaults to the following:
```json
{
"^/api.html$": ["GET"],
"^/api$": ["GET"],
"^/docs/oauth2-redirect": ["GET"],
"^/healthz": ["GET"]
}
```
- **`ENABLE_AUTHENTICATION_EXTENSION`**, enable authentication extension in STAC API responses
- **Type:** boolean
- **Required:** No, defaults to `true`
- **Example:** `false`, `1`, `True`
#### OpenAPI / Swagger UI
- **`OPENAPI_SPEC_ENDPOINT`**, path of OpenAPI specification, used for augmenting spec response with auth configuration
- **Type:** string or null
- **Required:** No, defaults to `null` (disabled)
- **Example:** `/api`
- **`OPENAPI_AUTH_SCHEME_NAME`**, name of the auth scheme to use in the OpenAPI spec
- **Type:** string
- **Required:** No, defaults to `oidcAuth`
- **Example:** `jwtAuth`
- **`OPENAPI_AUTH_SCHEME_OVERRIDE`**, override for the auth scheme in the OpenAPI spec
- **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 to indicate that a custom Swagger UI should be hosted, typically useful when providing accompanying `SWAGGER_UI_INIT_OAUTH` arguments
- **Type:** string or null
- **Required:** No, defaults to `null` (disabled)
- **Example:** `/api.html`
- **`SWAGGER_UI_INIT_OAUTH`**, initialization options for the [Swagger UI OAuth2 configuration](https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/) on custom Swagger UI
- **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
- **Required:** No, defaults to `null` (disabled)
- **Example:** `stac_auth_proxy.filters:Opa`, `stac_auth_proxy.filters:Template`, `my_package:OrganizationFilter`
- **`ITEMS_FILTER_ARGS`**, Positional arguments for CQL2 expression generator
- **Type:** List of positional arguments used to initialize the class
- **Required:** No, defaults to `[]`
- **Example:**: `["org1"]`
- **`ITEMS_FILTER_KWARGS`**, Keyword arguments for CQL2 expression generator
- **Type:** Dictionary of keyword arguments used to initialize the class
- **Required:** No, defaults to `{}`
- **Example:** `{"field_name": "properties.organization"}`
- **`ITEMS_FILTER_PATH`**, Regex pattern used to identify request paths that require the application of the items filter
- **Type:** Regex string
- **Required:** No, defaults to `^(/collections/([^/]+)/items(/[^/]+)?$|/search$)`
- **Example:** `^(/collections/([^/]+)/items(/[^/]+)?$|/search$|/custom$)`
- **`COLLECTIONS_FILTER_CLS`**, CQL2 expression generator for collection-level filtering
- **Type:** JSON object with class configuration
- **Required:** No, defaults to `null` (disabled)
- **Example:** `stac_auth_proxy.filters:Opa`, `stac_auth_proxy.filters:Template`, `my_package:OrganizationFilter`
- **`COLLECTIONS_FILTER_ARGS`**, Positional arguments for CQL2 expression generator
- **Type:** List of positional arguments used to initialize the class
- **Required:** No, defaults to `[]`
- **Example:**: `["org1"]`
- **`COLLECTIONS_FILTER_KWARGS`**, Keyword arguments for CQL2 expression generator
- **Type:** Dictionary of keyword arguments used to initialize the class
- **Required:** No, defaults to `{}`
- **Example:** `{"field_name": "properties.organization"}`
- **`COLLECTIONS_FILTER_PATH`**, Regex pattern used to identify request paths that require the application of the collections filter
- **Type:** Regex string
- **Required:** No, defaults to `^/collections(/[^/]+)?$`
- **Example:** `^.*?/collections(/[^/]+)?$`
### Tips
#### Root Paths
The proxy can be optionally served from a non-root path (e.g., `/api/v1`). Additionally, the proxy can optionally proxy requests to an upstream API served from a non-root path (e.g., `/stac`). To handle this, the proxy will:
- Remove the `ROOT_PATH` from incoming requests before forwarding to the upstream API
- Remove the proxy's prefix from all links in STAC API responses
- Add the `ROOT_PATH` prefix to all links in STAC API responses
- Update the OpenAPI specification to include the `ROOT_PATH` in the servers field
- Handle requests that don't match the `ROOT_PATH` with a 404 response
#### Non-OIDC Workaround
If the upstream server utilizes RS256 JWTs but does not utilize a proper OIDC server, the proxy can be configured to work around this by setting the `OIDC_DISCOVERY_URL` to a statically-hosted OIDC discovery document that points to a valid JWKS endpoint. Additionally, the OpenAPI can be configured to support direct JWT input, via:
```sh
OPENAPI_AUTH_SCHEME_NAME=jwtAuth
OPENAPI_AUTH_SCHEME_OVERRIDE={"type": "http", "scheme": "bearer", "bearerFormat": "JWT", "description": "Paste your raw JWT here. This API uses Bearer token authorization."}
```
### Customization
While the project is designed to work out-of-the-box as an application, it might not address every projects needs. When the need for customization arises, the codebase can instead be treated as a library of components that can be used to augment any [ASGI](https://asgi.readthedocs.io/en/latest/)-compliant webserver (e.g. [Django](https://docs.djangoproject.com/en/3.0/topics/async/), [Falcon](https://falconframework.org/), [FastAPI](https://github.com/tiangolo/fastapi), [Litestar](https://litestar.dev/), [Responder](https://responder.readthedocs.io/en/latest/), [Sanic](https://sanic.dev/), [Starlette](https://www.starlette.io/)). Review [`app.py`](https://github.com/developmentseed/stac-auth-proxy/blob/main/src/stac_auth_proxy/app.py) to get a sense of how we make use of the various components to construct a FastAPI application.
## Architecture
### Middleware Stack
The majority of the proxy's functionality occurs within a chain of middlewares. Each request passes through this chain, wherein each middleware performs a specific task:
1. **`EnforceAuthMiddleware`**
- Handles authentication and authorization
- Configurable public/private endpoints
- OIDC integration
- Places auth token payload in request state
2. **`BuildCql2FilterMiddleware`**
- Builds CQL2 filters based on request context/state
- Places [CQL2 expression](http://developmentseed.org/cql2-rs/latest/python/#cql2.Expr) in request state
3. **`ApplyCql2FilterMiddleware`**
- Retrieves [CQL2 expression](http://developmentseed.org/cql2-rs/latest/python/#cql2.Expr) from request state
- Augments request with CQL2 filter:
- Modifies query strings for `GET` requests
- Modifies JSON bodies for `POST`/`PUT`/`PATCH` requests
- Validates response against CQL2 filter for non-filterable endpoints
4. **`OpenApiMiddleware`**
- Modifies OpenAPI specification based on endpoint configuration, adding security requirements
- Only active if `openapi_spec_endpoint` is configured
5. **`AddProcessTimeHeaderMiddleware`**
- Adds processing time headers
- Useful for monitoring/debugging
### Data filtering via CQL2
The system supports generating CQL2 filters based on request context to provide row-level content filtering. These CQL2 filters are then set on outgoing requests prior to the upstream API.
> [!IMPORTANT]
> The upstream STAC API must support the [STAC API Filter Extension](https://github.com/stac-api-extensions/filter/blob/main/README.md), including the [Features Filter](http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter) conformance class on to the Features resource (`/collections/{cid}/items`)[^37].
#### Filters
If enabled, filters are applied to the following endpoints:
- `GET /search`
- **Supported:** β
- **Action:** Read Item
- **Applied Filter:** `ITEMS_FILTER`
- **Strategy:** Append query params with generated CQL2 query.
- `POST /search`
- **Supported:** β
- **Action:** Read Item
- **Applied Filter:** `ITEMS_FILTER`
- **Strategy:** Append body with generated CQL2 query.
- `GET /collections/{collection_id}/items`
- **Supported:** β
- **Action:** Read Item
- **Applied Filter:** `ITEMS_FILTER`
- **Strategy:** Append query params with generated CQL2 query.
- `GET /collections/{collection_id}/items/{item_id}`
- **Supported:** β
- **Action:** Read Item
- **Applied Filter:** `ITEMS_FILTER`
- **Strategy:** Validate response against CQL2 query.
- `GET /collections`
- **Supported:** β
- **Action:** Read Collection
- **Applied Filter:** `COLLECTIONS_FILTER`
- **Strategy:** Append query params with generated CQL2 query.
- `GET /collections/{collection_id}`
- **Supported:** β
- **Action:** Read Collection
- **Applied Filter:** `COLLECTIONS_FILTER`
- **Strategy:** Validate response against CQL2 query.
- `POST /collections/`
- **Supported:** β[^22]
- **Action:** Create Collection
- **Applied Filter:** `COLLECTIONS_FILTER`
- **Strategy:** Validate body with generated CQL2 query.
- `PUT /collections/{collection_id}}`
- **Supported:** β[^22]
- **Action:** Update Collection
- **Applied Filter:** `COLLECTIONS_FILTER`
- **Strategy:** Fetch Collection and validate CQL2 query; merge Item with body and validate with generated CQL2 query.
- `DELETE /collections/{collection_id}`
- **Supported:** β[^22]
- **Action:** Delete Collection
- **Applied Filter:** `COLLECTIONS_FILTER`
- **Strategy:** Fetch Collectiion and validate with CQL2 query.
- `POST /collections/{collection_id}/items`
- **Supported:** β[^21]
- **Action:** Create Item
- **Applied Filter:** `ITEMS_FILTER`
- **Strategy:** Validate body with generated CQL2 query.
- `PUT /collections/{collection_id}/items/{item_id}`
- **Supported:** β[^21]
- **Action:** Update Item
- **Applied Filter:** `ITEMS_FILTER`
- **Strategy:** Fetch Item and validate CQL2 query; merge Item with body and validate with generated CQL2 query.
- `DELETE /collections/{collection_id}/items/{item_id}`
- **Supported:** β[^21]
- **Action:** Delete Item
- **Applied Filter:** `ITEMS_FILTER`
- **Strategy:** Fetch Item and validate with CQL2 query.
- `POST /collections/{collection_id}/bulk_items`
- **Supported:** β[^21]
- **Action:** Create Items
- **Applied Filter:** `ITEMS_FILTER`
- **Strategy:** Validate items in body with generated CQL2 query.
#### Example Request Flow for multi-record endpoints
```mermaid
sequenceDiagram
Client->>Proxy: GET /collections
Note over Proxy: EnforceAuth checks credentials
Note over Proxy: BuildCql2Filter creates filter
Note over Proxy: ApplyCql2Filter applies filter to request
Proxy->>STAC API: GET /collection?filter=(collection=landsat)
STAC API->>Client: Response
```
#### Example Request Flow for single-record endpoints
The Filter Extension does not apply to fetching individual records. As such, we must validate the record _after_ it is returned from the upstream API but _before_ it is returned to the user:
```mermaid
sequenceDiagram
Client->>Proxy: GET /collections/abc123
Note over Proxy: EnforceAuth checks credentials
Note over Proxy: BuildCql2Filter creates filter
Proxy->>STAC API: GET /collection/abc123
Note over Proxy: ApplyCql2Filter validates the response
STAC API->>Client: Response
```
#### Authoring Filter Generators
The `ITEMS_FILTER_CLS` configuration option can be used to specify a class that will be used to generate a CQL2 filter for the request. The class must define a `__call__` method that accepts a single argument: a dictionary containing the request context; and returns a valid `cql2-text` expression (as a `str`) or `cql2-json` expression (as a `dict`).
> [!TIP]
> An example integration can be found in [`examples/custom-integration`](https://github.com/developmentseed/stac-auth-proxy/blob/main/examples/custom-integration).
##### Basic Filter Generator
```py
import dataclasses
from typing import Any
from cql2 import Expr
@dataclasses.dataclass
class ExampleFilter:
async def __call__(self, context: dict[str, Any]) -> str:
return "true"
```
> [!TIP]
> Despite being referred to as a _class_, a filter generator could be written as a function.
>
> <details>
>
> <summary>Example</summary>
>
> ```py
> from typing import Any
>
> from cql2 import Expr
>
>
> def example_filter():
> async def example_filter(context: dict[str, Any]) -> str | dict[str, Any]:
> return Expr("true")
> return example_filter
> ```
>
> </details>
##### Complex Filter Generator
An example of a more complex filter generator where the filter is generated based on the response of an external API:
```py
import dataclasses
from typing import Any
from httpx import AsyncClient
from stac_auth_proxy.utils.cache import MemoryCache
@dataclasses.dataclass
class ApprovedCollectionsFilter:
api_url: str
kind: Literal["item", "collection"] = "item"
client: AsyncClient = dataclasses.field(init=False)
cache: MemoryCache = dataclasses.field(init=False)
def __post_init__(self):
# We keep the client in the class instance to avoid creating a new client for
# each request, taking advantage of the client's connection pooling.
self.client = AsyncClient(base_url=self.api_url)
self.cache = MemoryCache(ttl=30)
async def __call__(self, context: dict[str, Any]) -> dict[str, Any]:
token = context["req"]["headers"].get("authorization")
try:
# Check cache for a previously generated filter
approved_collections = self.cache[token]
except KeyError:
# Lookup approved collections from an external API
approved_collections = await self.lookup(token)
self.cache[token] = approved_collections
# Build CQL2 filter
return {
"op": "a_containedby",
"args": [
{"property": "collection" if self.kind == "item" else "id"},
approved_collections
],
}
async def lookup(self, token: Optional[str]) -> list[str]:
# Lookup approved collections from an external API
headers = {"Authorization": f"Bearer {token}"} if token else {}
response = await self.client.get(
f"/get-approved-collections",
headers=headers,
)
response.raise_for_status()
return response.json()["collections"]
```
> [!TIP]
> Filter generation runs for every relevant request. Consider memoizing external API calls to improve performance.
[^21]: https://github.com/developmentseed/stac-auth-proxy/issues/21
[^22]: https://github.com/developmentseed/stac-auth-proxy/issues/22
[^30]: https://github.com/developmentseed/stac-auth-proxy/issues/30
[^37]: https://github.com/developmentseed/stac-auth-proxy/issues/37
Raw data
{
"_id": null,
"home_page": null,
"name": "stac-auth-proxy",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.9",
"maintainer_email": null,
"keywords": "Authentication, FastAPI, Proxy, STAC",
"author": null,
"author_email": "Anthony Lukach <anthonylukach@gmail.com>",
"download_url": "https://files.pythonhosted.org/packages/a6/1c/6f6f17205f3fb07e7279bd48e75d1246ec5beeae196401ab2c385f8c2349/stac_auth_proxy-0.7.0.tar.gz",
"platform": null,
"description": "<div align=\"center\">\n <h1 style=\"font-family: monospace\">stac auth proxy</h1>\n <p align=\"center\">Reverse proxy to apply auth*n to your STAC API.</p>\n</div>\n\n---\n\nSTAC Auth Proxy is a proxy API that mediates between the client and your internally accessible STAC API to provide flexible authentication, authorization, and content-filtering mechanisms.\n\n> [!IMPORTANT]\n> **We would :heart: to hear from you!**\n> Please [join the discussion](https://github.com/developmentseed/eoAPI/discussions/209) and let us know how you're using eoAPI! This helps us improve the project for you and others.\n> If you prefer to remain anonymous, you can email us at eoapi@developmentseed.org, and we'll be happy to post a summary on your behalf.\n\n## \u2728Features\u2728\n\n- **\ud83d\udd10 Authentication:** Apply [OpenID Connect (OIDC)](https://openid.net/developers/how-connect-works/) token validation and optional scope checks to specified endpoints and methods\n- **\ud83d\udec2 Content Filtering:** Use CQL2 filters via the [Filter Extension](https://github.com/stac-api-extensions/filter?tab=readme-ov-file) to tailor API responses based on request context (e.g. user role)\n- **\ud83e\udd1d External Policy Integration:** Integrate with external systems (e.g. [Open Policy Agent (OPA)](https://www.openpolicyagent.org/)) to generate CQL2 filters dynamically from policy decisions\n- **\ud83e\udde9 Authentication Extension:** Add the [Authentication Extension](https://github.com/stac-extensions/authentication) to API responses to expose auth-related metadata\n- **\ud83d\udcd8 OpenAPI Augmentation:** Enhance the [OpenAPI spec](https://swagger.io/specification/) with security details to keep auto-generated docs and UIs (e.g., [Swagger UI](https://swagger.io/tools/swagger-ui/)) accurate\n- **\ud83d\udddc\ufe0f Response Compression:** Optimize response sizes using [`starlette-cramjam`](https://github.com/developmentseed/starlette-cramjam/)\n\n## Usage\n\n### Running\n\nThe simplest way to run the project is by invoking the application via Docker:\n\n```sh\ndocker run \\\n -it --rm \\\n -p 8000:8000 \\\n -e UPSTREAM_URL=https://my-stac-api \\\n -e OIDC_DISCOVERY_URL=https://my-auth-server/.well-known/openid-configuration \\\n ghcr.io/developmentseed/stac-auth-proxy:latest\n```\n\nAlternatively, the module can be invoked directly or the application's factory can be passed to Uvicorn:\n\n```sh\npython -m stac_auth_proxy\n```\n\n```sh\nuvicorn --factory stac_auth_proxy:create_app\n```\n\n### Installation\n\nFor local development, we use [`uv`](https://docs.astral.sh/uv/) to manage project dependencies and environment.\n\n```sh\nuv sync\n```\n\nOtherwise, the application can be installed as a standard Python module:\n\n```sh\npip install -e .\n```\n\n> [!NOTE]\n> This project will be available on PyPi in the near future[^30].\n\n### Configuration\n\nThe application is configurable via environment variables.\n\n#### Core\n- **`UPSTREAM_URL`**, STAC API URL\n - **Type:** HTTP(S) URL\n - **Required:** Yes\n - **Example:** `https://your-stac-api.com/stac`\n- **`WAIT_FOR_UPSTREAM`**, wait for upstream API to become available before starting proxy\n - **Type:** boolean\n - **Required:** No, defaults to `true`\n - **Example:** `false`, `1`, `True`\n- **`CHECK_CONFORMANCE`**, ensure upstream API conforms to required conformance classes before starting proxy\n - **Type:** boolean\n - **Required:** No, defaults to `true`\n - **Example:** `false`, `1`, `True`\n- **`ENABLE_COMPRESSION`**, enable response compression\n - **Type:** boolean\n - **Required:** No, defaults to `true`\n - **Example:** `false`, `1`, `True`\n- **`HEALTHZ_PREFIX`**, path prefix for health check endpoints\n - **Type:** string\n - **Required:** No, defaults to `/healthz`\n - **Example:** `''` (disabled)\n- **`OVERRIDE_HOST`**, override the host header for the upstream API\n - **Type:** boolean\n - **Required:** No, defaults to `true`\n - **Example:** `false`, `1`, `True`\n- **`ROOT_PATH`**, path prefix for the proxy API\n - **Type:** string\n - **Required:** No, defaults to `''` (root path)\n - **Example:** `/api/v1`\n - **Note:** This is independent of the upstream API's path. The proxy will handle removing this prefix from incoming requests and adding it to outgoing links.\n\n#### Authentication\n- **`OIDC_DISCOVERY_URL`**, OpenID Connect discovery document URL\n - **Type:** HTTP(S) URL\n - **Required:** Yes\n - **Example:** `https://auth.example.com/.well-known/openid-configuration`\n- **`OIDC_DISCOVERY_INTERNAL_URL`**, internal network OpenID Connect discovery document URL\n - **Type:** HTTP(S) URL\n - **Required:** No, defaults to the value of `OIDC_DISCOVERY_URL`\n - **Example:** `http://auth/.well-known/openid-configuration`\n- **`DEFAULT_PUBLIC`**, default access policy for endpoints\n - **Type:** boolean\n - **Required:** No, defaults to `false`\n - **Example:** `false`, `1`, `True`\n- **`PRIVATE_ENDPOINTS`**, endpoints explicitly marked as requiring authentication and possibly scopes\n - **Type:** JSON object mapping regex patterns to HTTP methods OR tuples of an HTTP method and string representing required scopes\n - **Required:** No, defaults to the following:\n ```json\n {\n \"^/collections$\": [\"POST\"],\n \"^/collections/([^/]+)$\": [\"PUT\", \"PATCH\", \"DELETE\"],\n \"^/collections/([^/]+)/items$\": [\"POST\"],\n \"^/collections/([^/]+)/items/([^/]+)$\": [\"PUT\", \"PATCH\", \"DELETE\"],\n \"^/collections/([^/]+)/bulk_items$\": [\"POST\"]\n }\n ```\n- **`PUBLIC_ENDPOINTS`**, endpoints explicitly marked as not requiring authentication, used when `DEFAULT_PUBLIC == False`\n - **Type:** JSON object mapping regex patterns to HTTP methods\n - **Required:** No, defaults to the following:\n ```json\n {\n \"^/api.html$\": [\"GET\"],\n \"^/api$\": [\"GET\"],\n \"^/docs/oauth2-redirect\": [\"GET\"],\n \"^/healthz\": [\"GET\"]\n }\n ```\n- **`ENABLE_AUTHENTICATION_EXTENSION`**, enable authentication extension in STAC API responses\n - **Type:** boolean\n - **Required:** No, defaults to `true`\n - **Example:** `false`, `1`, `True`\n\n#### OpenAPI / Swagger UI\n- **`OPENAPI_SPEC_ENDPOINT`**, path of OpenAPI specification, used for augmenting spec response with auth configuration\n - **Type:** string or null\n - **Required:** No, defaults to `null` (disabled)\n - **Example:** `/api`\n- **`OPENAPI_AUTH_SCHEME_NAME`**, name of the auth scheme to use in the OpenAPI spec\n - **Type:** string\n - **Required:** No, defaults to `oidcAuth`\n - **Example:** `jwtAuth`\n- **`OPENAPI_AUTH_SCHEME_OVERRIDE`**, override for the auth scheme in the OpenAPI spec\n - **Type:** JSON object\n - **Required:** No, defaults to `null` (disabled)\n - **Example:** `{\"type\": \"http\", \"scheme\": \"bearer\", \"bearerFormat\": \"JWT\", \"description\": \"Paste your raw JWT here. This API uses Bearer token authorization.\\n\"}`\n- **`SWAGGER_UI_ENDPOINT`**, path of Swagger UI, used to indicate that a custom Swagger UI should be hosted, typically useful when providing accompanying `SWAGGER_UI_INIT_OAUTH` arguments\n - **Type:** string or null\n - **Required:** No, defaults to `null` (disabled)\n - **Example:** `/api.html`\n- **`SWAGGER_UI_INIT_OAUTH`**, initialization options for the [Swagger UI OAuth2 configuration](https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/) on custom Swagger UI\n - **Type:** JSON object\n - **Required:** No, defaults to `null` (disabled)\n - **Example:** `{\"clientId\": \"stac-auth-proxy\", \"usePkceWithAuthorizationCodeGrant\": true}`\n\n#### Filtering\n- **`ITEMS_FILTER_CLS`**, CQL2 expression generator for item-level filtering\n - **Type:** JSON object with class configuration\n - **Required:** No, defaults to `null` (disabled)\n - **Example:** `stac_auth_proxy.filters:Opa`, `stac_auth_proxy.filters:Template`, `my_package:OrganizationFilter`\n- **`ITEMS_FILTER_ARGS`**, Positional arguments for CQL2 expression generator\n - **Type:** List of positional arguments used to initialize the class\n - **Required:** No, defaults to `[]`\n - **Example:**: `[\"org1\"]`\n- **`ITEMS_FILTER_KWARGS`**, Keyword arguments for CQL2 expression generator\n - **Type:** Dictionary of keyword arguments used to initialize the class\n - **Required:** No, defaults to `{}`\n - **Example:** `{\"field_name\": \"properties.organization\"}`\n- **`ITEMS_FILTER_PATH`**, Regex pattern used to identify request paths that require the application of the items filter\n - **Type:** Regex string\n - **Required:** No, defaults to `^(/collections/([^/]+)/items(/[^/]+)?$|/search$)`\n - **Example:** `^(/collections/([^/]+)/items(/[^/]+)?$|/search$|/custom$)`\n- **`COLLECTIONS_FILTER_CLS`**, CQL2 expression generator for collection-level filtering\n - **Type:** JSON object with class configuration\n - **Required:** No, defaults to `null` (disabled)\n - **Example:** `stac_auth_proxy.filters:Opa`, `stac_auth_proxy.filters:Template`, `my_package:OrganizationFilter`\n- **`COLLECTIONS_FILTER_ARGS`**, Positional arguments for CQL2 expression generator\n - **Type:** List of positional arguments used to initialize the class\n - **Required:** No, defaults to `[]`\n - **Example:**: `[\"org1\"]`\n- **`COLLECTIONS_FILTER_KWARGS`**, Keyword arguments for CQL2 expression generator\n - **Type:** Dictionary of keyword arguments used to initialize the class\n - **Required:** No, defaults to `{}`\n - **Example:** `{\"field_name\": \"properties.organization\"}`\n- **`COLLECTIONS_FILTER_PATH`**, Regex pattern used to identify request paths that require the application of the collections filter\n - **Type:** Regex string\n - **Required:** No, defaults to `^/collections(/[^/]+)?$`\n - **Example:** `^.*?/collections(/[^/]+)?$`\n\n### Tips\n\n#### Root Paths\n\nThe proxy can be optionally served from a non-root path (e.g., `/api/v1`). Additionally, the proxy can optionally proxy requests to an upstream API served from a non-root path (e.g., `/stac`). To handle this, the proxy will:\n\n- Remove the `ROOT_PATH` from incoming requests before forwarding to the upstream API\n- Remove the proxy's prefix from all links in STAC API responses\n- Add the `ROOT_PATH` prefix to all links in STAC API responses\n- Update the OpenAPI specification to include the `ROOT_PATH` in the servers field\n- Handle requests that don't match the `ROOT_PATH` with a 404 response\n\n#### Non-OIDC Workaround\n\nIf the upstream server utilizes RS256 JWTs but does not utilize a proper OIDC server, the proxy can be configured to work around this by setting the `OIDC_DISCOVERY_URL` to a statically-hosted OIDC discovery document that points to a valid JWKS endpoint. Additionally, the OpenAPI can be configured to support direct JWT input, via:\n\n```sh\nOPENAPI_AUTH_SCHEME_NAME=jwtAuth\nOPENAPI_AUTH_SCHEME_OVERRIDE={\"type\": \"http\", \"scheme\": \"bearer\", \"bearerFormat\": \"JWT\", \"description\": \"Paste your raw JWT here. This API uses Bearer token authorization.\"}\n```\n\n### Customization\n\nWhile the project is designed to work out-of-the-box as an application, it might not address every projects needs. When the need for customization arises, the codebase can instead be treated as a library of components that can be used to augment any [ASGI](https://asgi.readthedocs.io/en/latest/)-compliant webserver (e.g. [Django](https://docs.djangoproject.com/en/3.0/topics/async/), [Falcon](https://falconframework.org/), [FastAPI](https://github.com/tiangolo/fastapi), [Litestar](https://litestar.dev/), [Responder](https://responder.readthedocs.io/en/latest/), [Sanic](https://sanic.dev/), [Starlette](https://www.starlette.io/)). Review [`app.py`](https://github.com/developmentseed/stac-auth-proxy/blob/main/src/stac_auth_proxy/app.py) to get a sense of how we make use of the various components to construct a FastAPI application.\n\n## Architecture\n\n### Middleware Stack\n\nThe majority of the proxy's functionality occurs within a chain of middlewares. Each request passes through this chain, wherein each middleware performs a specific task:\n\n1. **`EnforceAuthMiddleware`**\n\n - Handles authentication and authorization\n - Configurable public/private endpoints\n - OIDC integration\n - Places auth token payload in request state\n\n2. **`BuildCql2FilterMiddleware`**\n\n - Builds CQL2 filters based on request context/state\n - Places [CQL2 expression](http://developmentseed.org/cql2-rs/latest/python/#cql2.Expr) in request state\n\n3. **`ApplyCql2FilterMiddleware`**\n\n - Retrieves [CQL2 expression](http://developmentseed.org/cql2-rs/latest/python/#cql2.Expr) from request state\n - Augments request with CQL2 filter:\n - Modifies query strings for `GET` requests\n - Modifies JSON bodies for `POST`/`PUT`/`PATCH` requests\n - Validates response against CQL2 filter for non-filterable endpoints\n\n4. **`OpenApiMiddleware`**\n\n - Modifies OpenAPI specification based on endpoint configuration, adding security requirements\n - Only active if `openapi_spec_endpoint` is configured\n\n5. **`AddProcessTimeHeaderMiddleware`**\n - Adds processing time headers\n - Useful for monitoring/debugging\n\n### Data filtering via CQL2\n\nThe system supports generating CQL2 filters based on request context to provide row-level content filtering. These CQL2 filters are then set on outgoing requests prior to the upstream API.\n\n> [!IMPORTANT]\n> The upstream STAC API must support the [STAC API Filter Extension](https://github.com/stac-api-extensions/filter/blob/main/README.md), including the [Features Filter](http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter) conformance class on to the Features resource (`/collections/{cid}/items`)[^37].\n\n#### Filters\n\nIf enabled, filters are applied to the following endpoints:\n\n- `GET /search`\n - **Supported:** \u2705\n - **Action:** Read Item\n - **Applied Filter:** `ITEMS_FILTER`\n - **Strategy:** Append query params with generated CQL2 query.\n- `POST /search`\n - **Supported:** \u2705\n - **Action:** Read Item\n - **Applied Filter:** `ITEMS_FILTER`\n - **Strategy:** Append body with generated CQL2 query.\n- `GET /collections/{collection_id}/items`\n - **Supported:** \u2705\n - **Action:** Read Item\n - **Applied Filter:** `ITEMS_FILTER`\n - **Strategy:** Append query params with generated CQL2 query.\n- `GET /collections/{collection_id}/items/{item_id}`\n - **Supported:** \u2705\n - **Action:** Read Item\n - **Applied Filter:** `ITEMS_FILTER`\n - **Strategy:** Validate response against CQL2 query.\n- `GET /collections`\n - **Supported:** \u2705\n - **Action:** Read Collection\n - **Applied Filter:** `COLLECTIONS_FILTER`\n - **Strategy:** Append query params with generated CQL2 query.\n- `GET /collections/{collection_id}`\n - **Supported:** \u2705\n - **Action:** Read Collection\n - **Applied Filter:** `COLLECTIONS_FILTER`\n - **Strategy:** Validate response against CQL2 query.\n- `POST /collections/`\n - **Supported:** \u274c[^22]\n - **Action:** Create Collection\n - **Applied Filter:** `COLLECTIONS_FILTER`\n - **Strategy:** Validate body with generated CQL2 query.\n- `PUT /collections/{collection_id}}`\n - **Supported:** \u274c[^22]\n - **Action:** Update Collection\n - **Applied Filter:** `COLLECTIONS_FILTER`\n - **Strategy:** Fetch Collection and validate CQL2 query; merge Item with body and validate with generated CQL2 query.\n- `DELETE /collections/{collection_id}`\n - **Supported:** \u274c[^22]\n - **Action:** Delete Collection\n - **Applied Filter:** `COLLECTIONS_FILTER`\n - **Strategy:** Fetch Collectiion and validate with CQL2 query.\n- `POST /collections/{collection_id}/items`\n - **Supported:** \u274c[^21]\n - **Action:** Create Item\n - **Applied Filter:** `ITEMS_FILTER`\n - **Strategy:** Validate body with generated CQL2 query.\n- `PUT /collections/{collection_id}/items/{item_id}`\n - **Supported:** \u274c[^21]\n - **Action:** Update Item\n - **Applied Filter:** `ITEMS_FILTER`\n - **Strategy:** Fetch Item and validate CQL2 query; merge Item with body and validate with generated CQL2 query.\n- `DELETE /collections/{collection_id}/items/{item_id}`\n - **Supported:** \u274c[^21]\n - **Action:** Delete Item\n - **Applied Filter:** `ITEMS_FILTER`\n - **Strategy:** Fetch Item and validate with CQL2 query.\n- `POST /collections/{collection_id}/bulk_items`\n - **Supported:** \u274c[^21]\n - **Action:** Create Items\n - **Applied Filter:** `ITEMS_FILTER`\n - **Strategy:** Validate items in body with generated CQL2 query.\n\n#### Example Request Flow for multi-record endpoints\n\n```mermaid\nsequenceDiagram\n Client->>Proxy: GET /collections\n Note over Proxy: EnforceAuth checks credentials\n Note over Proxy: BuildCql2Filter creates filter\n Note over Proxy: ApplyCql2Filter applies filter to request\n Proxy->>STAC API: GET /collection?filter=(collection=landsat)\n STAC API->>Client: Response\n```\n\n#### Example Request Flow for single-record endpoints\n\nThe Filter Extension does not apply to fetching individual records. As such, we must validate the record _after_ it is returned from the upstream API but _before_ it is returned to the user:\n\n```mermaid\nsequenceDiagram\n Client->>Proxy: GET /collections/abc123\n Note over Proxy: EnforceAuth checks credentials\n Note over Proxy: BuildCql2Filter creates filter\n Proxy->>STAC API: GET /collection/abc123\n Note over Proxy: ApplyCql2Filter validates the response\n STAC API->>Client: Response\n```\n\n#### Authoring Filter Generators\n\nThe `ITEMS_FILTER_CLS` configuration option can be used to specify a class that will be used to generate a CQL2 filter for the request. The class must define a `__call__` method that accepts a single argument: a dictionary containing the request context; and returns a valid `cql2-text` expression (as a `str`) or `cql2-json` expression (as a `dict`).\n\n> [!TIP]\n> An example integration can be found in [`examples/custom-integration`](https://github.com/developmentseed/stac-auth-proxy/blob/main/examples/custom-integration).\n\n##### Basic Filter Generator\n\n```py\nimport dataclasses\nfrom typing import Any\n\nfrom cql2 import Expr\n\n\n@dataclasses.dataclass\nclass ExampleFilter:\n async def __call__(self, context: dict[str, Any]) -> str:\n return \"true\"\n```\n\n> [!TIP]\n> Despite being referred to as a _class_, a filter generator could be written as a function.\n>\n> <details>\n>\n> <summary>Example</summary>\n>\n> ```py\n> from typing import Any\n>\n> from cql2 import Expr\n>\n>\n> def example_filter():\n> async def example_filter(context: dict[str, Any]) -> str | dict[str, Any]:\n> return Expr(\"true\")\n> return example_filter\n> ```\n>\n> </details>\n\n##### Complex Filter Generator\n\nAn example of a more complex filter generator where the filter is generated based on the response of an external API:\n\n```py\nimport dataclasses\nfrom typing import Any\n\nfrom httpx import AsyncClient\nfrom stac_auth_proxy.utils.cache import MemoryCache\n\n\n@dataclasses.dataclass\nclass ApprovedCollectionsFilter:\n api_url: str\n kind: Literal[\"item\", \"collection\"] = \"item\"\n client: AsyncClient = dataclasses.field(init=False)\n cache: MemoryCache = dataclasses.field(init=False)\n\n def __post_init__(self):\n # We keep the client in the class instance to avoid creating a new client for\n # each request, taking advantage of the client's connection pooling.\n self.client = AsyncClient(base_url=self.api_url)\n self.cache = MemoryCache(ttl=30)\n\n async def __call__(self, context: dict[str, Any]) -> dict[str, Any]:\n token = context[\"req\"][\"headers\"].get(\"authorization\")\n\n try:\n # Check cache for a previously generated filter\n approved_collections = self.cache[token]\n except KeyError:\n # Lookup approved collections from an external API\n approved_collections = await self.lookup(token)\n self.cache[token] = approved_collections\n\n # Build CQL2 filter\n return {\n \"op\": \"a_containedby\",\n \"args\": [\n {\"property\": \"collection\" if self.kind == \"item\" else \"id\"},\n approved_collections\n ],\n }\n\n async def lookup(self, token: Optional[str]) -> list[str]:\n # Lookup approved collections from an external API\n headers = {\"Authorization\": f\"Bearer {token}\"} if token else {}\n response = await self.client.get(\n f\"/get-approved-collections\",\n headers=headers,\n )\n response.raise_for_status()\n return response.json()[\"collections\"]\n```\n\n> [!TIP]\n> Filter generation runs for every relevant request. Consider memoizing external API calls to improve performance.\n\n[^21]: https://github.com/developmentseed/stac-auth-proxy/issues/21\n[^22]: https://github.com/developmentseed/stac-auth-proxy/issues/22\n[^30]: https://github.com/developmentseed/stac-auth-proxy/issues/30\n[^37]: https://github.com/developmentseed/stac-auth-proxy/issues/37\n",
"bugtrack_url": null,
"license": "MIT License Copyright (c) 2024 Development Seed Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.",
"summary": "STAC authentication proxy with FastAPI",
"version": "0.7.0",
"project_urls": null,
"split_keywords": [
"authentication",
" fastapi",
" proxy",
" stac"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "42e16cd670c7a4f47a59f7671c5433d3feeb5e40c9c5b6133419cf1a8dd83acc",
"md5": "07ec58afcf62a0259d8846dc38e07dc6",
"sha256": "7a4db0cb5d4424428827ea4816950966ae7edb4e7d680ee45f6638ce43661175"
},
"downloads": -1,
"filename": "stac_auth_proxy-0.7.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "07ec58afcf62a0259d8846dc38e07dc6",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.9",
"size": 35768,
"upload_time": "2025-07-22T17:19:12",
"upload_time_iso_8601": "2025-07-22T17:19:12.949215Z",
"url": "https://files.pythonhosted.org/packages/42/e1/6cd670c7a4f47a59f7671c5433d3feeb5e40c9c5b6133419cf1a8dd83acc/stac_auth_proxy-0.7.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "a61c6f6f17205f3fb07e7279bd48e75d1246ec5beeae196401ab2c385f8c2349",
"md5": "db42c9347d865981fdafee99d519d5ce",
"sha256": "8c8545aead5003821e9b456f0ee93fc0484aeb9b2b90bfd5e181ad4d6597df96"
},
"downloads": -1,
"filename": "stac_auth_proxy-0.7.0.tar.gz",
"has_sig": false,
"md5_digest": "db42c9347d865981fdafee99d519d5ce",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.9",
"size": 126012,
"upload_time": "2025-07-22T17:19:13",
"upload_time_iso_8601": "2025-07-22T17:19:13.859860Z",
"url": "https://files.pythonhosted.org/packages/a6/1c/6f6f17205f3fb07e7279bd48e75d1246ec5beeae196401ab2c385f8c2349/stac_auth_proxy-0.7.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-07-22 17:19:13",
"github": false,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"lcname": "stac-auth-proxy"
}