Saltar a contenido

Flujo de Autenticación en Camunda 8

Resumen: Camunda 8 soporta dos métodos de autenticación: basic auth (con credenciales default demo/demo) y OIDC. El flujo OIDC web soporta múltiples Identity Providers con IssuerAwareJWSKeySelector, conversión de claims via TokenClaimsConverter, y caching de claims OIDC con CachingOidcClaimsProvider. La autenticación gRPC usa un AuthenticationInterceptor con un AuthenticationHandler sealed interface (variantes Oidc y BasicAuth). La comunicación service-to-service embebe claims en broker requests via BrokerRequestAuthorizationConverter, y el mapeo de claims OIDC a roles/grupos usa MappingRuleMatcher con expresiones JSONPath.


Métodos de autenticación

Basic Auth

El método más simple, diseñado para desarrollo y demo:

camunda:
  security:
    authentication:
      method: basic

Características: - Credenciales por defecto: demo / demo. - Almacenadas en la base de datos interna de Identity. - Autenticación vía header Authorization: Basic <base64(user:pass)>. - No recomendado para producción: sin soporte para SSO, MFA, o token rotation. - Útil para desarrollo local, testing, y demos.

Flujo:

flowchart TD
    A[Cliente] -->|Authorization: Basic ZGVtbzpkZW1v| B[BasicAuthenticationFilter]
    B --> C[Validar contra UserRepository]
    C --> D[Crear SecurityContext]
    D --> E[Continuar con request]

OIDC (OpenID Connect)

El método recomendado para producción:

camunda:
  security:
    authentication:
      method: oidc
    oidc:
      issuerUri: https://idp.example.com/realms/camunda
      clientId: camunda-app
      clientSecret: ${OIDC_SECRET}

OIDC Web: autenticación multi-IdP

IssuerAwareJWSKeySelector

Camunda 8 soporta múltiples Identity Providers simultáneamente. El IssuerAwareJWSKeySelector es el componente que permite validar tokens de diferentes issuers:

flowchart TD
    A[Token JWT entrante] --> B["Extraer claim 'iss' (issuer) del token"]
    B --> C{IssuerAwareJWSKeySelector}
    C --> D[Buscar JWK keys del issuer en cache]
    D -->|cache miss| E[Resolver OpenID Configuration del issuer]
    E --> F[Obtener jwks_uri]
    F --> G[Descargar las JWK keys]
    G --> H[Cachear]
    H --> I["Seleccionar la key que coincida con el 'kid' del token"]
    D -->|cache hit| I
    I --> J[Validar firma del JWT con la key seleccionada]

Configuración multi-IdP:

camunda:
  security:
    oidc:
      issuers:
        - uri: https://keycloak.internal/realms/camunda
          clientId: camunda-internal
        - uri: https://azure-ad.company.com/v2.0
          clientId: camunda-azure
        - uri: https://auth0.company.com/
          clientId: camunda-auth0

Ventajas: - Soportar diferentes departamentos con diferentes IdPs. - Migración gradual de un IdP a otro. - Federated identity: partners externos con su propio IdP.

TokenClaimsConverter

Convierte los claims del token JWT al modelo interno de Camunda:

flowchart TD
    A["JWT Claims (formato varía por IdP)"] --> B{TokenClaimsConverter}
    B --> C[Extraer campos estándar:<br/>sub, email, name, preferred_username]
    C --> D[Extraer campos custom configurados]
    D --> E[Normalizar a formato interno CamundaClaims]
    E --> F["CamundaClaims {<br/>subject, email, name,<br/>roles, groups,<br/>customClaims<br/>}"]

Configuración de mappeo de claims:

camunda:
  security:
    oidc:
      claims:
        subjectClaim: sub
        emailClaim: email
        nameClaim: name
        rolesClaim: realm_access.roles  # soporta nested paths
        groupsClaim: groups

CachingOidcClaimsProvider

Para evitar resolver claims en cada request (lo cual puede involucrar llamadas a APIs externas del IdP), Camunda implementa un cache:

flowchart TD
    A[Request con token] --> B{CachingOidcClaimsProvider}
    B --> C["Extraer cache key del token<br/>(jti claim o hash del token)"]
    C --> D{Buscar en cache}
    D -->|Cache HIT| E[Retornar claims cacheados]
    D -->|Cache MISS| F["Validar token<br/>(firma, expiración, issuer)"]
    F --> G[Convertir claims via TokenClaimsConverter]
    G --> H[Resolver mapping rules<br/>MappingRuleMatcher]
    H --> I["Cachear resultado con TTL =<br/>token.exp - now()"]
    I --> J[Retornar claims]

Características del cache: - Key: basada en el token (JTI claim o hash). - TTL: sincronizado con la expiración del token — el cache entry expira cuando expira el token. - Invalidación: al cambiar mapping rules, el cache se invalida. - Tamaño: configurable, con eviction LRU.


Autenticación gRPC

AuthenticationInterceptor

La autenticación gRPC se implementa como un interceptor en el pipeline de gRPC:

flowchart TD
    A[gRPC Request] --> B{AuthenticationInterceptor}
    B --> C["Extraer metadata del request<br/>(headers gRPC)"]
    C --> D["Buscar token de autenticación<br/>Authorization: Bearer (OIDC)<br/>Authorization: Basic (Basic)"]
    D --> E[Delegar a AuthenticationHandler apropiado]
    E --> F{Autenticación}
    F -->|exitosa| G[Crear SecurityContext]
    G --> H[Adjuntar al gRPC Context]
    H --> I[Continuar con siguiente<br/>interceptor/handler]
    F -->|falla| J[Retornar Status.UNAUTHENTICATED]

AuthenticationHandler (sealed interface)

AuthenticationHandler es una sealed interface con exactamente dos implementaciones:

sealed interface AuthenticationHandler {
    record Oidc(JwtDecoder decoder, ClaimsConverter converter) 
        implements AuthenticationHandler {}

    record BasicAuth(UserRepository repository, PasswordEncoder encoder) 
        implements AuthenticationHandler {}
}

¿Por qué sealed?

  • Exhaustividad: el compilador garantiza que todo switch/match sobre AuthenticationHandler cubre todos los casos.
  • Extensibilidad controlada: nuevos métodos de autenticación requieren modificar la interface (decisión consciente, no accidental).
  • Claridad: exactamente dos métodos soportados, documentados en el tipo.

Flujo por variante:

AuthenticationHandler.Oidc:
    1. Decodificar JWT con JwtDecoder
    2. Validar firma, expiración, issuer, audience
    3. Convertir claims con ClaimsConverter
    4. Retornar AuthenticationResult con CamundaClaims

AuthenticationHandler.BasicAuth:
    1. Decodificar Base64 credentials
    2. Buscar usuario en UserRepository
    3. Verificar password con PasswordEncoder (BCrypt)
    4. Retornar AuthenticationResult con UserDetails

Service-to-service: BrokerRequestAuthorizationConverter

Problema

En la arquitectura de Camunda 8, los requests no van directamente del cliente al broker. El flujo típico es:

flowchart LR
    A[Cliente] --> B["Gateway (REST/gRPC)"] --> C["Broker (procesamiento)"]

El gateway autentica al cliente, pero el broker necesita saber quién hizo el request original para aplicar autorización. El broker no tiene acceso directo al token del cliente.

Solución: embeber claims en broker requests

El BrokerRequestAuthorizationConverter resuelve esto incrustando los claims de autenticación en los requests internos al broker:

1. Gateway recibe request del cliente
2. AuthenticationInterceptor autentica y extrae claims
3. BrokerRequestAuthorizationConverter:
   a. Serializa los claims relevantes
   b. Los adjunta como metadata en el request al broker
   c. Incluye: subject, roles, tenants, permisos relevantes
4. Broker recibe request:
   a. Deserializa claims de la metadata
   b. Crea SecurityContext interno
   c. Aplica authorization checks normalmente

Seguridad del canal

  • La comunicación gateway-broker es interna y se asume como trusted.
  • Los claims embebidos no están firmados criptográficamente (a diferencia de un JWT) — la seguridad depende de la red interna.
  • En producción, se recomienda TLS entre gateway y broker para prevenir tampering.

Formato de los claims embebidos

BrokerRequest metadata:
    authorized-subject: "alice"
    authorized-roles: "operator,developer"
    authorized-tenants: "tenant-a,tenant-b"
    authorized-permissions: "READ:PROCESS_DEFINITION:*,CREATE:PROCESS_INSTANCE:order-process"

MappingRuleMatcher: JSONPath contra claims

Concepto

Las mapping rules permiten traducir claims del token OIDC a entidades internas de Camunda (roles, grupos, tenants) de forma declarativa:

camunda:
  security:
    mappingRules:
      - name: "Engineering team gets developer role"
        match:
          claimPath: "$.groups"
          operation: CONTAINS
          value: "engineering"
        assign:
          role: "developer"
          tenant: "engineering-workspace"

      - name: "Platform admins get admin role"
        match:
          claimPath: "$.realm_access.roles"
          operation: CONTAINS
          value: "platform-admin"
        assign:
          role: "admin"
          tenant: "*"  # wildcard: todos los tenants

JSONPath para claims

El MappingRuleMatcher usa JSONPath para navegar la estructura del token JWT:

JSONPath Claim target
$.sub Subject (usuario)
$.email Email del usuario
$.groups Array de grupos (top-level)
$.realm_access.roles Roles de Keycloak (nested)
$.resource_access.camunda.roles Roles por recurso en Keycloak
$.custom_attributes.department Atributo custom

Operaciones de matching

Operación Descripción Ejemplo
EQUALS El claim es exactamente el valor $.department EQUALS "engineering"
CONTAINS El claim (array) contiene el valor $.groups CONTAINS "admins"
STARTS_WITH El claim comienza con el valor $.email STARTS_WITH "admin@"
MATCHES El claim coincide con un regex $.sub MATCHES "^svc-.*"

Evaluación de reglas

función evaluarMappingRules(oidcClaims):
    resultados = {roles: Set(), groups: Set(), tenants: Set()}

    para cada rule en configuración.mappingRules:
        claimValue = JSONPath.evaluate(oidcClaims, rule.match.claimPath)

        si rule.match.operation.test(claimValue, rule.match.value):
            resultados.roles.addAll(rule.assign.roles)
            resultados.groups.addAll(rule.assign.groups)
            resultados.tenants.addAll(rule.assign.tenants)

    return resultados

Características: - Todas las reglas se evalúan: no hay short-circuit. Un usuario puede matchear múltiples reglas. - Acumulativas: los roles, grupos y tenants de todas las reglas que matchean se acumulan. - Orden no importa: las reglas son independientes entre sí.


Consideraciones para un MVP

  • MVP mínimo: basic auth con usuarios en base de datos es suficiente para empezar. Es trivial de implementar y no requiere IdP externo.
  • Segundo paso: OIDC con un solo IdP (sin multi-IdP). IssuerAwareJWSKeySelector puede simplificarse a un JWSKeySelector estándar.
  • Simplificable: el CachingOidcClaimsProvider es una optimización — sin cache, cada request re-valida el token (aceptable para MVP con bajo tráfico).
  • Simplificable: MappingRuleMatcher puede empezar con operación EQUALS solamente sobre campos de primer nivel (sin JSONPath).
  • Esencial: el BrokerRequestAuthorizationConverter (o equivalente) es necesario tan pronto como gateway y broker estén separados. Sin él, el broker no puede autorizar requests.
  • Esencial: el AuthenticationInterceptor como gRPC interceptor es el patrón correcto — es el standard en el ecosistema gRPC para autenticación.