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 viaTokenClaimsConverter, y caching de claims OIDC conCachingOidcClaimsProvider. La autenticación gRPC usa unAuthenticationInterceptorcon unAuthenticationHandlersealed interface (variantes Oidc y BasicAuth). La comunicación service-to-service embebe claims en broker requests viaBrokerRequestAuthorizationConverter, y el mapeo de claims OIDC a roles/grupos usaMappingRuleMatchercon expresiones JSONPath.
Métodos de autenticación¶
Basic Auth¶
El método más simple, diseñado para desarrollo y demo:
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
AuthenticationHandlercubre 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).
IssuerAwareJWSKeySelectorpuede simplificarse a unJWSKeySelectorestándar. - Simplificable: el
CachingOidcClaimsProvideres una optimización — sin cache, cada request re-valida el token (aceptable para MVP con bajo tráfico). - Simplificable:
MappingRuleMatcherpuede empezar con operaciónEQUALSsolamente 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
AuthenticationInterceptorcomo gRPC interceptor es el patrón correcto — es el standard en el ecosistema gRPC para autenticación.