ADR-014: OIDC single IdP en Phase 1¶
- Status: Accepted
- Date: 2026-05-14
- Tags: security, authentication, identity
Context and Problem Statement¶
El MVP necesita authentication. ¿Build user management propio (passwords + reset + 2FA + lockout), o delegate to IdP via OIDC?
Decision Drivers¶
- Building auth correctly es difícil (security bugs catastrophic)
- OIDC es industry standard
- IdPs (Auth0, Okta, Keycloak, Cognito) son maduros
- User management complejo (2FA, reset, lockout) no diferencia el MVP
- Camunda 8.5+ tiene OIDC built-in (compatible path)
Considered Options¶
- OIDC single IdP (Phase 1)
- OIDC multi-IdP (como Camunda 8 — federación)
- Build user management propio
- OAuth2 + JWT custom
- Sin authentication (delegate al network layer)
Decision Outcome¶
Chosen option: OIDC single IdP en Phase 1, multi-IdP en Phase 2+ si needed.
Positive Consequences¶
- Cero código de password management
- IdP maneja 2FA, reset, lockout, etc.
- Compatible con Auth0/Okta/Keycloak/Cognito (cliente elige)
- SSO support automatic
- Compliance heredada del IdP
- Audit trail centralizada en IdP
Negative Consequences¶
- Dependency externa (IdP availability afecta MVP)
- Setup IdP requires expertise inicial
- JWT validation overhead per request (mitigable con cache)
- Multi-IdP requiere Phase 2+ work
Configuration¶
# Engine config
auth:
type: oidc
issuer: https://auth.example.com
audience: workflow-engine
jwks_uri: https://auth.example.com/.well-known/jwks.json
claims:
user_id: sub
email: email
groups: groups
Flow¶
1. Cliente → IdP (Auth0/Okta/Keycloak)
Authorization code flow
2. IdP → Cliente: authorization code
3. Cliente → IdP: code + secret
Exchange para tokens
4. Cliente → Engine: API call con JWT
Authorization: Bearer eyJ...
5. Engine:
- Validate JWT signature via JWKS
- Extract sub claim (user_id)
- Lookup user_tenants para resolve role
- Apply authorization (ver ADR-013)
JWT validation con cache¶
import httpx
from cachetools import TTLCache
import jwt
jwks_cache = TTLCache(maxsize=10, ttl=300) # 5 min cache
async def get_jwks(issuer):
if issuer in jwks_cache:
return jwks_cache[issuer]
async with httpx.AsyncClient() as client:
resp = await client.get(f"{issuer}/.well-known/jwks.json")
jwks = resp.json()
jwks_cache[issuer] = jwks
return jwks
async def validate_token(token):
unverified_header = jwt.get_unverified_header(token)
jwks = await get_jwks(ISSUER)
key = find_key_by_kid(jwks, unverified_header['kid'])
claims = jwt.decode(
token,
key=key,
algorithms=['RS256'],
audience=AUDIENCE,
issuer=ISSUER
)
return claims
JWKS cache invalida cada 5 min — handles key rotation gracefully.
API keys para workers¶
Workers M2M no usan OIDC — usan API keys:
# Worker config
WORKFLOW_API_KEY=wfk_abc123def456...
# Engine valida:
# 1. Hash el key con bcrypt
# 2. Lookup en api_keys table
# 3. Si match y not expired → continúa
API keys son simple bearer tokens para machine workers. OIDC sería overkill.
Cuándo multi-IdP (Phase 2+)¶
Build multi-IdP support si: - Cliente enterprise quiere federación con su IdP existente - Distintos tenants quieren distintos IdPs - Compliance requires self-hosted IdP
Implementación Phase 2:
auth:
type: oidc
providers:
- id: corporate
issuer: https://idp.corp.example.com
audience: workflow-engine
tenant_mapping: corp-tenant
- id: partners
issuer: https://idp.partners.example.com
audience: workflow-engine
tenant_mapping: partner-tenant
IssuerAwareJWSKeySelector pattern de Camunda.
Links¶
- concepts/authentication-flow — Camunda auth flow detalle
- adrs/adr-013-simple-rbac-three-roles — Authorization complementario
- OpenID Connect spec
- JWT RFC 7519