Saltar a contenido

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

  1. OIDC single IdP (Phase 1)
  2. OIDC multi-IdP (como Camunda 8 — federación)
  3. Build user management propio
  4. OAuth2 + JWT custom
  5. 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.