From b19c3338e0c6903707db875f4a5927db71b708d2 Mon Sep 17 00:00:00 2001 From: Espen Klungland Date: Mon, 15 Jun 2026 14:47:34 +0200 Subject: [PATCH] STAC Auth simple demo --- src/auth/README.md | 46 +++++++++++++++++++++ src/auth/api/compose.yml | 55 ++++++++++++++++++++++++ src/auth/api/data/cog/test.txt | 1 + src/auth/api/data/collection.json | 48 +++++++++++++++++++++ src/auth/api/data/item.json | 69 +++++++++++++++++++++++++++++++ src/auth/api/data/nginx.conf | 14 +++++++ src/auth/api/init_db.sh | 20 +++++++++ src/auth/main.py | 39 +++++++++++++++++ src/auth/requirements.txt | 1 + 9 files changed, 293 insertions(+) create mode 100644 src/auth/README.md create mode 100644 src/auth/api/compose.yml create mode 100644 src/auth/api/data/cog/test.txt create mode 100644 src/auth/api/data/collection.json create mode 100644 src/auth/api/data/item.json create mode 100644 src/auth/api/data/nginx.conf create mode 100755 src/auth/api/init_db.sh create mode 100644 src/auth/main.py create mode 100644 src/auth/requirements.txt diff --git a/src/auth/README.md b/src/auth/README.md new file mode 100644 index 0000000..2f88a30 --- /dev/null +++ b/src/auth/README.md @@ -0,0 +1,46 @@ +# STAC Asset Auth Demo + +Eksempel på autentisering av STAC assets ved bruk av pre-signed-urls. +Denne demoen tar noen snarveier for å eksemplifisere en flyt hvor +STAC API peker på en auth server som genererer og redirecter til +en pre-singed-url. + + +## Getting started + +```bash +# Start STAC API and storage +# pwd: src/auth/ +cd api/ +docker compose up -d +./init_db.sh +``` + +```bash +# Start Auth server +# pwd src/auth/ +pip install -r requrements.txt +fastapi dev +``` + +```bash +# Test connections + +# Authenticated directly to storage +curl -i http://localhost:8080/test.txt?token=demo123 +# Not authenticated directly to storage +curl -i http://localhost:8080/test.txt + +# Authenticated to auth server (redirect) +curl -H "Authorization: bearer mysecrettoken123" http://localhost:8000/test.txt -L +# Not authenticated to auth server +curl http://localhost:8000/test.txt -L + +# Find the item using STAC search +curl -X 'GET' \ + 'http://localhost:8082/search?collections=auth-test&ids=the-message&limit=10&filter-lang=cql2-text' \ + -H 'accept: application/geo+json' +``` + + + diff --git a/src/auth/api/compose.yml b/src/auth/api/compose.yml new file mode 100644 index 0000000..356622f --- /dev/null +++ b/src/auth/api/compose.yml @@ -0,0 +1,55 @@ +services: + app: + image: stac-utils/stac-fastapi-pgstac + build: . + environment: + - APP_HOST=0.0.0.0 + - APP_PORT=8082 + - RELOAD=true + - ENVIRONMENT=local + - PGUSER=username + - PGPASSWORD=password + - PGDATABASE=postgis + - PGHOST=database + - PGPORT=5432 + - WEB_CONCURRENCY=10 + - VSI_CACHE=TRUE + - GDAL_HTTP_MERGE_CONSECUTIVE_RANGES=YES + - GDAL_DISABLE_READDIR_ON_OPEN=EMPTY_DIR + - DB_MIN_CONN_SIZE=1 + - DB_MAX_CONN_SIZE=1 + - USE_API_HYDRATE=${USE_API_HYDRATE:-false} + - ENABLE_TRANSACTIONS_EXTENSIONS=true + - ENABLE_CATALOGS_EXTENSION=true + ports: + - "8082:8082" + volumes: + - ./stac_fastapi:/app/stac_fastapi + depends_on: + - database + command: bash -c "scripts/wait-for-it.sh database:5432 && python -m stac_fastapi.pgstac.app" + + database: + image: ghcr.io/stac-utils/pgstac:v0.9.8 + environment: + - POSTGRES_USER=username + - POSTGRES_PASSWORD=password + - POSTGRES_DB=postgis + - PGUSER=username + - PGPASSWORD=password + - PGDATABASE=postgis + ports: + - "5439:5432" + command: postgres -N 500 + + storage: + image: nginx:alpine + ports: + - 8080:80 + volumes: + - ./data/cog/:/usr/share/nginx/html:ro + - ./data/nginx.conf:/etc/nginx/nginx.conf:ro + +networks: + default: + name: stac-fastapi-network diff --git a/src/auth/api/data/cog/test.txt b/src/auth/api/data/cog/test.txt new file mode 100644 index 0000000..89ad63d --- /dev/null +++ b/src/auth/api/data/cog/test.txt @@ -0,0 +1 @@ +I am super secret! diff --git a/src/auth/api/data/collection.json b/src/auth/api/data/collection.json new file mode 100644 index 0000000..c77560c --- /dev/null +++ b/src/auth/api/data/collection.json @@ -0,0 +1,48 @@ +{ + "id": "auth-test", + "description": "for testing stac auth", + "stac_version": "1.0.0", + "links": [ + ], + "stac_extensions": [], + "title": "Auth Test", + "type": "Collection", + "assets": { + }, + "license": "proprietary", + "extent": { + "spatial": { + "bbox": [ + [ + -180, + -90, + 180, + 90 + ] + ] + }, + "temporal": { + "interval": [ + [ + "2026-06-15T10:41:07.993Z", + null + ] + ] + } + }, + "keywords": [ + "auth", "test" + ], + "providers": [ + { + "name": "Skygeo", + "description": "For the cloud", + "roles": [ + "publisher" + ], + "url": "https://github.com/kartAI/skygeo" + } + ], + "summaries": { + } +} diff --git a/src/auth/api/data/item.json b/src/auth/api/data/item.json new file mode 100644 index 0000000..9329c6a --- /dev/null +++ b/src/auth/api/data/item.json @@ -0,0 +1,69 @@ +{ + "bbox": [ + -180, + -90, + 180, + 90 + ], + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 10.443471396313623, + 60.20395514925783 + ], + [ + 10.443471396313623, + 60.209497078619115 + ], + [ + 10.428807703585424, + 60.209497078619115 + ], + [ + 10.428807703585424, + 60.20395514925783 + ], + [ + 10.443471396313623, + 60.20395514925783 + ] + ] + ] + }, + "properties": { + "title": "secret message", + "description": "this is the protected message", + "datetime": "2026-06-15T11:25:59.408Z", + "created": "2026-06-15T11:25:59.408Z", + "updated": "2026-06-15T11:25:59.408Z", + "start_datetime": "2026-06-15T11:25:59.408Z", + "end_datetime": "2026-06-15T11:25:59.408Z", + "license": "proprietary", + "providers": [ + { + "name": "Kartverket", + "roles": [ + "owner" + ], + "url": "https://www.kartverket.no" + } + ] + }, + "id": "the-message", + "stac_version": "1.0.0", + "assets": { + "cog": { + "href": "http://localhost:8000/test.txt", + "type": "txt", + "title": "Message", + "description": "A secret message" + } + }, + "links": [ + ], + "stac_extensions": [], + "collection": "auth-test" +} diff --git a/src/auth/api/data/nginx.conf b/src/auth/api/data/nginx.conf new file mode 100644 index 0000000..3d85d3b --- /dev/null +++ b/src/auth/api/data/nginx.conf @@ -0,0 +1,14 @@ +events { +} + +http { + server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + location / { + if ($arg_token != "demo123") { return 403; } + try_files $uri $uri/ =404; + } + } +} diff --git a/src/auth/api/init_db.sh b/src/auth/api/init_db.sh new file mode 100755 index 0000000..4574bb7 --- /dev/null +++ b/src/auth/api/init_db.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -e + +# 1. Get the absolute path to the directory where THIS script is located +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd) + +# Create collection +curl -X 'POST' \ + 'http://localhost:8082/collections' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d @"$SCRIPT_DIR/data/collection.json" + +# Add an item +curl -X 'POST' \ + 'http://localhost:8082/collections/auth-test/items' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d @"$SCRIPT_DIR/data/item.json" diff --git a/src/auth/main.py b/src/auth/main.py new file mode 100644 index 0000000..bfc06fb --- /dev/null +++ b/src/auth/main.py @@ -0,0 +1,39 @@ +from fastapi import FastAPI, Depends, HTTPException, status +from fastapi.responses import RedirectResponse +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials + +app = FastAPI() + +# Static token for demo purposes +API_TOKEN = "mysecrettoken123" +STORAGE_HOST = "http://localhost:8080" +# The auth api server manages the credentials to storage. +# this is comparable to having the server issue pre-signed urls. +STORAGE_TOKEN = "demo123" + +security = HTTPBearer() + +def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)): + if credentials.scheme.lower() != "bearer": + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication scheme" + ) + if credentials.credentials != API_TOKEN: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid or missing token" + ) + return credentials.credentials + +app = FastAPI(dependencies=[Depends(verify_token)]) + +def get_presigned_url(obj: str): + # Dummy function to simulate creating a presinged url + return f"{STORAGE_HOST}/{obj}?token={STORAGE_TOKEN}" + +@app.get("/{obj}") +def read_item(obj: str): + return RedirectResponse( + url=get_presigned_url(obj) + ) diff --git a/src/auth/requirements.txt b/src/auth/requirements.txt new file mode 100644 index 0000000..db261b0 --- /dev/null +++ b/src/auth/requirements.txt @@ -0,0 +1 @@ +fastapi[standard]==0.137.0