diff --git a/docs/07-row_level_auth.ipynb b/docs/07-row_level_auth.ipynb
index 0bfa5a4..e9a2a48 100644
--- a/docs/07-row_level_auth.ipynb
+++ b/docs/07-row_level_auth.ipynb
@@ -25,8 +25,11 @@
"proxy applies it to every relevant request — without touching stac-fastapi-pgstac at all.\n",
"\n",
"
\n",
- "Note: This chapter needs the local docker-compose auth stack. It will not run against\n",
- "the hosted workshop deployment.\n",
+ "Which stack you need. Sections 7.4 – 7.5.1 run against\n",
+ "either the local docker-compose stack or the hosted deployment — see\n",
+ "§7.3.3 for the deployed variant. Section 7.6 onward needs docker-compose,\n",
+ "because it creates and deletes records and the deployed STAC API has the transaction\n",
+ "extension disabled on purpose.\n",
"
"
]
},
@@ -218,7 +221,40 @@
"time: `private-alice-demo` and `private-bob-demo`. To try it, open the proxy's Swagger UI\n",
"at `https://{project}-protected-stac.eoapi.dev/api.html`, click **Authorize**, sign in as\n",
"`alice`, and call `GET /collections`: `private-alice-demo` is listed and\n",
- "`private-bob-demo` is not. Signed out, neither appears."
+ "`private-bob-demo` is not. Signed out, neither appears.\n",
+ "\n",
+ "### Running the read-side against the deployment\n",
+ "\n",
+ "Sections 7.4 to 7.5.1 only read, so they work against the deployed proxy too. Swap the\n",
+ "setup cell in 7.4 for this, using the same workshop token `workshop_setup.setup()` asks\n",
+ "for:\n",
+ "\n",
+ "```python\n",
+ "from stac_auth import auth_headers, deployed_auth, get_cognito_token\n",
+ "\n",
+ "cfg = deployed_auth(workshop_token)\n",
+ "stac_api_endpoint = cfg[\"stac_endpoint\"]\n",
+ "\n",
+ "def token_for(user):\n",
+ " return get_cognito_token(user, cfg[\"password\"], cfg[\"client_id\"], cfg[\"region\"])\n",
+ "\n",
+ "alice, bob, anonymous = auth_headers(token_for(\"alice\")), auth_headers(token_for(\"bob\")), {}\n",
+ "```\n",
+ "\n",
+ "Then use the seeded collections in place of the ones 7.4 creates:\n",
+ "\n",
+ "```python\n",
+ "public_id = \"eoapi-workshop-sentinel-2-c1-l2a\"\n",
+ "alice_id, bob_id = \"private-alice-demo\", \"private-bob-demo\"\n",
+ "```\n",
+ "\n",
+ "and skip the item cells, since the seeded collections have no items. 7.5 and 7.5.1 then\n",
+ "run unchanged, and you stop before 7.6.\n",
+ "\n",
+ "Two things this shows that the local stack does not. The tokens are signed by a real\n",
+ "identity provider rather than a mock, and they carry no `stac/write` scope — so even\n",
+ "the write-rejection in 7.6 would fail for the *wrong* reason (route-level auth, not\n",
+ "row-level), which is precisely why that section stays local."
]
},
{
diff --git a/docs/stac_auth.py b/docs/stac_auth.py
index 3d360c7..5664851 100644
--- a/docs/stac_auth.py
+++ b/docs/stac_auth.py
@@ -1,7 +1,12 @@
"""
-Helpers for authenticated STAC transaction requests in the local docker-compose stack.
+Helpers for authenticated STAC requests against the workshop's auth stack.
-Requires MOCK_OIDC_ENDPOINT and STAC_API_ENDPOINT (stac-auth-proxy).
+The local docker-compose stack is the default: `get_mock_oidc_token` needs
+MOCK_OIDC_ENDPOINT and STAC_API_ENDPOINT (stac-auth-proxy).
+
+`deployed_auth` and `get_cognito_token` are the hosted equivalents, for running the
+read-side of chapter 7 against the deployed proxy without docker. They cannot do writes:
+the deployed STAC API has the transaction extension disabled on purpose.
"""
from __future__ import annotations
@@ -78,3 +83,85 @@ def get_mock_oidc_token(
def auth_headers(token: str) -> dict[str, str]:
return {"Authorization": f"Bearer {token}"}
+
+
+def deployed_auth(
+ workshop_token: str,
+ config_url: str | None = None,
+ timeout: float = 20.0,
+) -> dict[str, str]:
+ """Look up the deployed proxy endpoint and Cognito details.
+
+ Args:
+ workshop_token: the bearer token for the config endpoint, the same one
+ `workshop_setup.setup()` asks for.
+
+ Returns a dict with `stac_endpoint`, `client_id`, `region` and `password`.
+ """
+ config_url = config_url or os.getenv(
+ "CONFIG_API_ENDPOINT", "https://workshop-config.eoapi.dev"
+ )
+ response = httpx.get(
+ config_url,
+ headers=auth_headers(workshop_token),
+ timeout=timeout,
+ )
+ response.raise_for_status()
+ config = response.json()
+
+ discovery = config.get("oidc_discovery_url", "")
+ # https://cognito-idp..amazonaws.com//.well-known/...
+ region = discovery.split("://", 1)[-1].split(".")[1] if discovery else ""
+
+ missing = [
+ key
+ for key, value in {
+ "stac_auth_proxy_endpoint": config.get("stac_auth_proxy_endpoint"),
+ "oidc_client_id": config.get("oidc_client_id"),
+ }.items()
+ if not value
+ ]
+ if missing:
+ raise RuntimeError(
+ f"The config endpoint did not return {missing}. "
+ "The deployment may predate the Cognito auth proxy."
+ )
+
+ return {
+ "stac_endpoint": config["stac_auth_proxy_endpoint"],
+ "client_id": config["oidc_client_id"],
+ "region": region,
+ "password": config.get("workshop_user_password", ""),
+ }
+
+
+def get_cognito_token(
+ username: str,
+ password: str,
+ client_id: str,
+ region: str,
+ timeout: float = 20.0,
+) -> str:
+ """Return a Cognito access token for `username`, without a browser redirect.
+
+ Uses `USER_PASSWORD_AUTH`, which the workshop's app client enables for exactly this
+ reason. The resulting token carries the `username` claim the row-level filter reads,
+ but *not* the `stac/write` scope, so it can read the catalog and never write to it.
+ """
+ response = httpx.post(
+ f"https://cognito-idp.{region}.amazonaws.com/",
+ headers={
+ "Content-Type": "application/x-amz-json-1.1",
+ "X-Amz-Target": "AWSCognitoIdentityProviderService.InitiateAuth",
+ },
+ json={
+ "AuthFlow": "USER_PASSWORD_AUTH",
+ "ClientId": client_id,
+ "AuthParameters": {"USERNAME": username, "PASSWORD": password},
+ },
+ timeout=timeout,
+ )
+ if response.status_code != 200:
+ raise RuntimeError(f"Cognito rejected the sign-in: {response.text}")
+
+ return response.json()["AuthenticationResult"]["AccessToken"]
diff --git a/infrastructure/app.py b/infrastructure/app.py
index 29f9e0a..bfaadf5 100644
--- a/infrastructure/app.py
+++ b/infrastructure/app.py
@@ -378,6 +378,19 @@ def __init__(
"http://localhost:8086",
],
),
+ # Lets a notebook sign in as alice or bob without a browser redirect, so
+ # chapter 7's read-side demo can run against this deployment and not only
+ # against docker-compose.
+ #
+ # This does not weaken the write protection. Cognito issues
+ # USER_PASSWORD_AUTH tokens with the scope `aws.cognito.signin.user.admin`
+ # and never the custom `stac/*` scopes, so such a token still fails the
+ # scope check in PRIVATE_ENDPOINTS. It carries `username`, which is all the
+ # row-level filter reads.
+ #
+ # `user_srp` is kept because naming any flow replaces Cognito's implicit
+ # defaults, and dropping SRP would be an unintended narrowing.
+ auth_flows=aws_cognito.AuthFlow(user_password=True, user_srp=True),
)
# Two workshop identities sharing one password. Cognito creates users in