Skip to main content

hc-obo-auth

Wire OBO (On-Behalf-Of) authentication into a service or BFF route. Use when the user asks to "add a route", "wire OBO", "set up authentication", "connect to Lakebase as the user", "extract user identity", or anything about user-passthrough auth. Enforces the canonical pattern where every Postgres connection is opened with the calling user's OAuth credential and Unity Catalog enforces access at the data layer.

Ir a la instalación

Datos de origen

Repositorio
databricks-solutions/microbricks
Última actividad en el origen
6 de junio de 2026 a las 15:14
Idioma detectado de SKILL.md
inglés
Estrellas
4
Forks
3

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Explorador de archivos
2 archivos

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
hc-obo-auth
description
Wire OBO (On-Behalf-Of) authentication into a service or BFF route. Use when the user asks to "add a route", "wire OBO", "set up authentication", "connect to Lakebase as the user", "extract user identity", or anything about user-passthrough auth. Enforces the canonical pattern where every Postgres connection is opened with the calling user's OAuth credential and Unity Catalog enforces access at the data layer.
# hc-obo-auth The non-negotiable auth rule in this architecture: **every Postgres session opens with the calling user's OAuth credential.** No service-principal fallback in production code, no shortcuts. This skill is the source of truth for the canonical patterns. Any deviation needs to be argued for in a PR description. ## When to use - "Add a new route to `patient-svc` that lists allergies" - "Wire OBO into the BFF client for `lab-svc`" - "How do I connect to Lakebase as the user?" - "Set up `db.py` for the new service" - The user is touching anything that crosses the trust boundary (HTTP route, DB connection, downstream service call) ## When NOT to use - The user is writing pure unit tests against a mock — OBO doesn't apply. - The user is writing a one-off ETL script — script auth is its own thing, use a service principal there but document it as outside the OBO trust path. ## The four pillars 1. **Token in.** Every backend route reads `X-Forwarded-Access-Token` from the request — Databricks Apps injects this on every call. 2. **Token to UC.** When the service needs UC operations, it builds `WorkspaceClient(token=user_token)`, never `WorkspaceClient()` without a token in production. 3. **Token to Lakebase.** When the service connects to Postgres, it calls `WorkspaceClient(token=user_token, auth_type="pat").postgres.generate_database_credential(endpoint=ENDPOINT_NAME)` and uses the returned token as the Postgres password. The `auth_type="pat"` is required when the code runs inside a Databricks App: the platform auto-injects `DATABRICKS_CLIENT_ID` + `DATABRICKS_CLIENT_SECRET` for the app's service principal, and the SDK refuses ambiguous oauth+pat config unless one method is forced. Connections are pooled with `max_lifetime=2700` (45 min) so a stale token never lives longer than 1h. 4. **Token forward.** When the BFF or any service calls another service, it copies the user token into both `Authorization: Bearer ...` and `X-Forwarded-Access-Token` headers. The downstream service treats it as if the user called directly. The detailed canonical code (drop-in copies for every service) lives in [`references/canonical-patterns.md`](references/canonical-patterns.md). ## Quick patterns ### Extracting the user token in a FastAPI route ```python from fastapi import Header, HTTPException async def user_token( x_forwarded_access_token: str | None = Header(default=None, alias="X-Forwarded-Access-Token"), ) -> str: if not x_forwarded_access_token: raise HTTPException(401, "Missing X-Forwarded-Access-Token") return x_forwarded_access_token ``` Use it as a dependency: ```python @router.get("/patients", response_model=list[PatientOut], operation_id="listPatients") async def list_patients(token: str = Depends(user_token)): async with patient_db_pool(token).connection() as conn: ... ``` ### Building a per-request DB pool Don't build one global pool. Build one *per user token*, cached briefly. The full helper is in [`references/canonical-patterns.md`](references/canonical-patterns.md), but the shape: ```python from psycopg_pool import AsyncConnectionPool import psycopg from databricks.sdk import WorkspaceClient import os, asyncio _pools: dict[str, AsyncConnectionPool] = {} _lock = asyncio.Lock() class OAuthConnection(psycopg.AsyncConnection): @classmethod async def connect(cls, conninfo: str = "", **kwargs): token = kwargs.pop("_user_token", None) if not token: raise RuntimeError("OAuthConnection requires _user_token kwarg") # auth_type="pat" disambiguates from the SP credentials Apps auto-injects. ws = WorkspaceClient(token=token, auth_type="pat") cred = ws.postgres.generate_database_credential(endpoint=os.environ["ENDPOINT_NAME"]) kwargs["password"] = cred.token return await super().connect(conninfo, **kwargs) def patient_db_pool(user_token: str) -> AsyncConnectionPool: # Cache pools per token (short-lived). In production you'd cache by token's iss+sub claim # to avoid leaking memory if many distinct tokens hit the service. if user_token not in _pools: host = os.environ["PGHOST"] user = os.environ["PGUSER"] _pools[user_token] = AsyncConnectionPool( conninfo=f"host={host} port=5432 dbname=databricks_postgres user={user} sslmode=require", connection_class=OAuthConnection, max_lifetime=2700, kwargs={"_user_token": user_token}, open=False, ) return _pools[user_token] ``` > ⚠️ The `_pools[token]` cache leaks if many distinct tokens hit the service. The full pattern in `references/` uses an LRU keyed by `hash(token)` with a TTL. ### Forwarding the token from the BFF URL resolution: explicit `<SVC>_SVC_URL` override → runtime-derived from `DATABRICKS_APP_URL`. See `references/canonical-patterns.md` for the full `_BaseSvcClient` shape. ```python class PatientClient(_BaseSvcClient): def __init__(self, user_token: str): super().__init__(user_token, service_slug="patient") ``` The BFF route extracts the token the same way as services and constructs one client per request: ```python @router.get("/patient-summary/{patient_id}", operation_id="getPatientSummary") async def patient_summary(patient_id: str, token: str = Depends(user_token)): async with PatientClient(token) as p, AppointmentClient(token) as a: patient, appts = await asyncio.gather( p.get(patient_id), a.list_for_patient(patient_id, limit=3), ) return {"patient": patient, "appointments": appts} ``` ## Local development When running `apx dev start` outside Databricks Apps, there's no `X-Forwarded-Access-Token` injected. Two options, both supported: 1. **Pass-through from the dev's CLI auth.** The `apx` lakebase addon can inject the dev's token into the env so requests pretend to be OBO. The dev's identity is then used for Lakebase. Set `LOCAL_DEV_TOKEN_FROM_CLI=true` in `.env` and the canonical pattern reads it. 2. **Explicit header.** When testing with `curl`, include `-H "X-Forwarded-Access-Token: $(databricks auth token -p hc-dev | jq -r .access_token)"`. What the canonical pattern **does not** support: a service-principal fallback when running locally. Local dev still uses the developer's user identity to talk to Lakebase. This keeps prod-vs-dev behavior identical and prevents "works on my machine, fails in prod" auth bugs. ## Anti-patterns to reject in code review | Pattern | Why it's wrong | |---|---| | `WorkspaceClient()` (no token) in route handlers | Falls back to the app's service principal, defeats OBO | | Storing a connection pool keyed only on env (one global pool) | All users share one connection → all queries run as the SP | | Reading the token from a query param or cookie instead of the header | The header is what Databricks Apps injects; anything else is a different trust path | | Forwarding the token but stripping `Authorization` and only sending `X-Forwarded-Access-Token` | Some downstream libraries only check `Authorization` — send both | | Caching `generate_database_credential` results across requests | Tokens are user-bound — caching them across users leaks identity | ## Verification After wiring OBO into a route: 1. `curl -H "X-Forwarded-Access-Token: <user-1-token>" .../patients/me` returns user-1's data. 2. `curl -H "X-Forwarded-Access-Token: <user-2-token>" .../patients/me` returns user-2's data, not user-1's. 3. `curl .../patients/me` (no header) returns 401. 4. In Lakebase, `SELECT current_user;` from the route returns the calling user's email, not the app's service principal email. Add a debug route that echoes `current_user` to confirm during initial wiring. ## Pointer Full canonical code (`db.py`, `auth.py`, BFF client) is in [`references/canonical-patterns.md`](references/canonical-patterns.md). Copy from there into the service; do not invent new shapes.
Ver en GitHub