# Dogfood API — `iurban-auth` E2E example

A minimal FastAPI app that consumes the SDK and validates it against the **real**
Keycloak realm (`keycloak.tools.iurban-lab.es/realms/iurban`) — no mocks. This is the
first real consumer of the iUrban SSO, and the reference an OIDC-native microapp copies.

## Endpoints

| Route     | Guard                          | Meaning |
|-----------|--------------------------------|---------|
| `/health` | none                           | liveness |
| `/me`     | `current_user`                 | any authenticated caller; echoes the extracted identity |
| `/admin`  | `require_roles("dogfood-admin")` | needs the `dogfood-admin` realm role (Option A: the app authorizes) |

## Keycloak setup (one-off, realm `iurban`)

The realm forces login via Google (interactive), so for an automatable check we use a
confidential client with the **client_credentials** grant. Create it with the admin API
(or `kcadm`):

1. Client `dogfood-api`: `publicClient=false`, `serviceAccountsEnabled=true`,
   `directAccessGrantsEnabled=true`, `standardFlowEnabled=false`.
2. Audience protocol mapper `oidc-audience-mapper` with `included.client.audience=dogfood-api`
   (so the token `aud` carries `dogfood-api`, which the SDK checks).
3. Realm role `dogfood-admin`, assigned to the client's service-account user (makes the
   `/admin` authorization check meaningful).

> This client is a validation artifact, **safe to delete**. Its secret is a standing
> credential — keep it in Vaultwarden if you make the client permanent, or delete the
> client after running the check.

## Run

```bash
cd auth-py
SECRET=...  # dogfood-api client secret
TOKEN=$(curl -fsS -d client_id=dogfood-api --data-urlencode "client_secret=$SECRET" \
  -d grant_type=client_credentials \
  https://keycloak.tools.iurban-lab.es/realms/iurban/protocol/openid-connect/token \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['access_token'])")

IURBAN_OIDC_ISSUER=https://keycloak.tools.iurban-lab.es/realms/iurban \
IURBAN_OIDC_AUDIENCE=dogfood-api \
uv run --with uvicorn python -m uvicorn main:app --app-dir examples/protected-api --port 8080
```

```bash
curl -H "Authorization: Bearer $TOKEN" localhost:8080/me      # 200 + identity
curl -H "Authorization: Bearer $TOKEN" localhost:8080/admin   # 200 (has dogfood-admin)
curl -H "Authorization: Bearer bad.token" localhost:8080/me   # 401
curl localhost:8080/me                                        # 401 (no bearer)
```

## Verified result (2026-06-11)

Against the live realm, the SDK validated a **real** token statelessly via the realm JWKS
(RS256 signature, issuer and audience checks, roles extracted from `realm_access.roles`):

- `/me` → `200` with `sub`, `roles` (incl. `dogfood-admin`); `email`/`tenant_id` `null`
  (the service account carries neither — a user token via Google would).
- `/admin` → `200` (the `dogfood-admin` role satisfied `require_roles`).
- garbage / missing token → `401`.

The negative authorization path (valid token **lacking** the role → `403`) is covered by
the unit suite (`tests/test_auth.py::test_require_roles_forbids_when_missing`).
