iris.api — Configuración Fase 3¶
FastAPI · joserfc · Keycloak OIDC ·
app.iris-lib.click· Puerto 8001 (dev)
1. Dependencias¶
Agregadas como optional group api en pyproject.toml:
authlibse instaló pero se migró ajoserfc(ya incluido como dependencia de authlib) para evitar el deprecation warning.authlibpuede removerse en una limpieza futura.
2. Estructura¶
src/iris/applications/api/
├── __init__.py
├── config.py # Settings via pydantic-settings
├── dependencies.py # Validación JWT, get_current_user, require_role, require_org
├── main.py # FastAPI app, middlewares, routers
└── routers/
├── __init__.py
├── auth.py # POST /auth/token, GET /auth/userinfo
├── users.py # GET /users/me, GET /users/, POST /users/
└── genomics.py # (Fase 4)
3. Configuración — config.py¶
Lee variables de entorno con prefijo IRIS_ desde .env:
class Settings(BaseSettings):
keycloak_url: str # IRIS_KEYCLOAK_URL
keycloak_realm: str # IRIS_KEYCLOAK_REALM
keycloak_client_id: str # IRIS_KEYCLOAK_CLIENT_ID
keycloak_client_secret: str # IRIS_KEYCLOAK_CLIENT_SECRET
jwt_algorithms: list[str] # IRIS_JWT_ALGORITHMS (default: ["RS256"])
jwt_audience: str # IRIS_JWT_AUDIENCE (default: "iris-api")
postgrest_url: str # IRIS_POSTGREST_URL
api_title: str # IRIS_API_TITLE
api_version: str # IRIS_API_VERSION
debug: bool # IRIS_DEBUG
Propiedades derivadas:
settings.keycloak_base_url # https://auth.iris-lib.click/realms/iris
settings.jwks_uri # .../protocol/openid-connect/certs
settings.token_endpoint # .../protocol/openid-connect/token
settings.userinfo_endpoint # .../protocol/openid-connect/userinfo
settings.admin_base_url # https://auth.iris-lib.click/admin/realms/iris
Importante: usar
stren lugar deAnyHttpUrlde Pydantic para las URLs.AnyHttpUrlagrega trailing slash causando doble//en las URLs construidas. El validatorstrip_trailing_slashlimpia cualquier slash final.
Archivo .env:
IRIS_KEYCLOAK_URL=https://auth.iris-lib.click
IRIS_KEYCLOAK_REALM=iris
IRIS_KEYCLOAK_CLIENT_ID=iris-api
IRIS_KEYCLOAK_CLIENT_SECRET=<secret>
IRIS_JWT_AUDIENCE=iris-api
IRIS_POSTGREST_URL=https://api.iris-lib.click
IRIS_DEBUG=true
4. Validación JWT — dependencies.py¶
Usa joserfc para validar tokens RS256 emitidos por Keycloak.
Flujo de validación¶
Request con Bearer token
→ get_current_user()
→ get_jwks() # descarga JWKS de Keycloak
→ jwt.decode() # valida firma RS256
→ claims.validate() # valida iss, aud, exp con leeway=60s
→ retorna claims dict
Funciones disponibles¶
# Inyectable via Depends — devuelve claims del JWT
CurrentUser = Annotated[dict, Depends(get_current_user)]
# Decorator de protección por rol
require_role("iris_admin")
require_role("bioinformatician", "iris_admin") # múltiples roles permitidos
# Decorator de protección por org
require_org(org_id=7) # verifica org_id del token, all_orgs=true lo bypasea
Uso en routers¶
from iris.applications.api.dependencies import CurrentUser, require_role
# Proteger endpoint por rol
@router.get(
"/sensitive",
dependencies=[Depends(require_role("iris_admin"))],
)
async def sensitive_endpoint() -> dict: ...
# Acceder a claims del usuario actual
@router.get("/me")
async def me(current_user: CurrentUser) -> dict:
return {"role": current_user.get("role")}
leeway=60— 60 segundos de tolerancia de clock skew en la validación deiatyexp.
5. Routers¶
routers/auth.py — /auth¶
| Método | Endpoint | Auth | Descripción |
|---|---|---|---|
POST |
/auth/token |
No | Proxy al token endpoint de Keycloak |
GET |
/auth/userinfo |
Bearer | Devuelve claims del JWT actual |
POST /auth/token abstrae la URL de Keycloak — los clients usan iris.api como único
punto de entrada y no necesitan conocer la URL del realm.
routers/users.py — /users¶
| Método | Endpoint | Rol requerido | Descripción |
|---|---|---|---|
GET |
/users/me |
Cualquiera | Perfil del usuario autenticado |
GET |
/users/ |
iris_admin |
Lista todos los usuarios del realm |
POST |
/users/ |
iris_admin |
Crea usuario en Keycloak |
POST /users/ — creación de usuario¶
{
"username": "dr-garcia",
"email": "garcia@hospital.com",
"first_name": "Juan",
"last_name": "García",
"role": "org_member",
"org_id": 7,
"sub_role": "clinician"
}
Internamente: 1. Crea el usuario en Keycloak via Admin API 2. Asigna el realm role correspondiente 3. Asigna al grupo correcto según rol:
role |
org_id |
sub_role |
Grupo asignado |
|---|---|---|---|
org_member |
7 | clinician |
/orgs/7/clinician |
bioinformatician |
— | — | /iris-internal/bioinformatician |
employee |
— | — | /iris-internal/employee |
patient |
— | — | /patients |
6. main.py¶
app = FastAPI(title=settings.api_title, ...)
app.add_middleware(CORSMiddleware, allow_origins=["*"], ...)
app.include_router(auth.router) # /auth
app.include_router(users.router) # /users
# app.include_router(genomics.router) # /genomics — Fase 4
@app.get("/health")
async def health() -> dict:
return {"status": "ok", "version": settings.api_version}
7. Arranque¶
Desarrollo¶
Producción (systemd + Podman)¶
Pendiente — se configura al final de la Fase 5 cuando todas las apps estén listas.
8. Verificación end-to-end¶
Obtener token via proxy¶
# Paso 1 — device code (directo a Keycloak, solo para obtener el code)
curl -s -X POST \
https://auth.iris-lib.click/realms/iris/protocol/openid-connect/auth/device \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=iris-genomics-cli" \
-d "client_secret=<secret>" \
-d "scope=openid"
# Autenticar en el navegador: verification_uri_complete
# Paso 2 — token via iris.api (no directo a Keycloak)
TOKEN=$(curl -s -X POST http://localhost:8001/auth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
-d "device_code=<device_code>" \
-d "client_id=iris-genomics-cli" \
-d "client_secret=<secret>" \
| python3 -c "import sys, json; print(json.load(sys.stdin)['access_token'])")
Endpoints de prueba¶
# Health
curl -s http://localhost:8001/health | python3 -m json.tool
# Userinfo (valida JWT completo)
curl -s http://localhost:8001/auth/userinfo \
-H "Authorization: Bearer $TOKEN" \
| python3 -m json.tool
# Perfil del usuario
curl -s http://localhost:8001/users/me \
-H "Authorization: Bearer $TOKEN" \
| python3 -m json.tool
Resultado validado — GET /users/me¶
{
"sub": "c1cda5e1-4df8-4ab6-91cc-78e4bab7e304",
"username": "test-bio",
"email": "test-bio@iris.local",
"name": "Test Bio",
"role": "org_member",
"sub_role": "clinician",
"org_id": 1,
"all_orgs": null,
"patient_id": null
}
9. Notas importantes¶
JWKS caching — actualmente get_jwks() hace un request HTTP en cada validación de token.
En producción conviene cachear el JWKS con TTL de ~5 minutos para reducir latencia.
Implementar con cachetools o un simple dict con timestamp en lifespan.
Admin API token — get_admin_token() en users.py usa client_credentials con
iris-api como service account. Requiere que iris-api tenga Service accounts roles: ON
en Keycloak y el role realm-management/manage-users asignado al service account.
CORS — configurado con allow_origins=["*"] para desarrollo.
En producción restringir a los dominios de las apps:
allow_origins=[
"https://genomics.iris-lib.click",
"https://erm.iris-lib.click",
"https://crm.iris-lib.click",
]
Puerto — en desarrollo corre en 8001 porque 8000 estaba ocupado.
En producción se expone via Caddy en app.iris-lib.click.