Impersonation Solution

Overview

The Impersonation Solution provides a secure way to propagate user identity information through long-running BPMN process instances in Orchescala. It addresses the challenge of maintaining user context beyond JWT token expiration while preventing replay attacks and tampering.

Problem Statement

The Challenge

Long-running BPMN processes (days, weeks, or months) need to maintain user identity information beyond the typical JWT token expiration period. The identity information is stored in an IdentityCorrelation object as a process variable in Camunda.

Security Risk

The IdentityCorrelation stored in process variables is accessible to anyone who can query the Camunda engine. This creates a security vulnerability:

Solution Requirements

Architecture

Core Components

1. IdentityCorrelation (Domain Object)

The IdentityCorrelation case class contains user identity information with signature fields:

case class IdentityCorrelation(
    username: String,
    email: Option[String] = None,
    impersonateProcessValue: Option[String] = None,
    issuedAt: Long = System.currentTimeMillis(),
    processInstanceId: Option[String] = None,
    signature: Option[String] = None
)

Fields:

2. IdentityCorrelationSigner (Utility)

Provides cryptographic signing and verification using HMAC-SHA256:

object IdentityCorrelationSigner:
  def sign(
      correlation: IdentityCorrelation,
      processInstanceId: String,
      signingKey: String
  ): IdentityCorrelation

  def verify(
      correlation: IdentityCorrelation,
      processInstanceId: String,
      signingKey: String
  ): Boolean

Algorithm: HMAC-SHA256 Encoding: Base64 for storage in process variables

3. EngineConfig (Configuration)

Configuration object with signing key support:

case class EngineConfig(
    tenantId: Option[String] = None,
    impersonateProcessKey: Option[String] = None,
    identitySigningKey: Option[String] = sys.env.get("ORCHESCALA_IDENTITY_SIGNING_KEY")
)

Default: Reads from ORCHESCALA_IDENTITY_SIGNING_KEY environment variable Override: Can be customized in EngineConfig implementations

Implementation Flow

Handled by the gateway and engine services. If you are interested in the internal stuff:).

Process Start Flow

The two-step approach solves the chicken-and-egg problem of not having the processInstanceId before starting the process:

Step 1: Start Process

// Start process to get processInstanceId
val processInstance = processInstanceApi.startProcessInstanceByKey(..., unsignedCorrelation)
val processInstanceId = processInstance.getId

Step 2: Sign and Update

// Sign the correlation with the processInstanceId
val signedCorrelation = signCorrelation(unsignedCorrelation, processInstanceId)

// Update process variables with signed correlation
runtimeService.setVariable(processInstanceId, "identityCorrelation", signedCorrelation)

Implemented in:

User Task Completion Flow

Similar two-step approach for task completion:

Step 1: Get Process Instance ID

// Get processInstanceId from task
val processInstanceId = getProcessInstanceIdFromTask(taskId)

Step 2: Sign and Complete

// Sign the correlation
val signedCorrelation = signCorrelation(identityCorrelation, processInstanceId)

// Complete task with signed correlation
taskService.complete(taskId, variables)

Implemented in:

Worker Verification Flow

Automatic signature verification for ServiceWorkers:

override def runWorkZIO(inputObject: In): RunnerOutputZIO =
  for
    // Automatic verification before service call
    _ <- verifyIdentityCorrelation()

    // Continue with service call
    rRequest <- createRequest(inputObject)
    response  <- sendRequest(rRequest)
    result    <- mapResponse(response)
  yield result

Verification Logic: 1. Extract IdentityCorrelation from process variables 2. Get processInstanceId from context 3. Verify signature matches using signing key from EngineConfig 4. Log warnings if verification fails (optional verification)

Applies to: ServiceWorkers only (not CustomWorkers)

Security Features

Prevents Replay Attacks

The signature binds the IdentityCorrelation to a specific processInstanceId. If someone copies the correlation to another process, the signature verification will fail because the processInstanceId won't match.

Example Attack Scenario (Prevented):

Process A (ID: 12345) - User: alice@example.com
Process B (ID: 67890) - Attacker copies alice's correlation

Verification in Process B:
- Correlation says: processInstanceId = "12345"
- Actual process: processInstanceId = "67890"
- Signature verification: FAILS ❌

Prevents Tampering

Any modification to the correlation data invalidates the HMAC signature.

Example Attack Scenario (Prevented):

Original: username = "alice@example.com"
Modified: username = "admin@example.com"

Verification:
- Signature was computed with "alice@example.com"
- Current data contains "admin@example.com"
- Signature verification: FAILS ❌

Works for Long-Running Processes

Unlike JWT tokens that expire, the signature remains valid for the entire process lifetime. No expiration or renewal needed.

Timeline:

Day 1:  Process starts → Correlation signed
Day 30: ServiceWorker executes → Signature verified ✓
Day 60: Another worker executes → Signature verified ✓
Day 90: Process completes → Signature still valid ✓

Graceful Degradation

The default verification is "optional" - it logs warnings but doesn't fail the worker execution. This ensures:

Verification Modes:

  1. Optional Verification (Default in ServiceHandler):

    IdentityVerification.verifySignatureOptional(correlation, processInstanceId, signingKey)
    // Logs warnings but doesn't fail
  2. Strict Verification (Available for custom use):

    IdentityVerification.verifySignature(correlation, processInstanceId, signingKey)
    // Fails with WorkerError.BadSignatureError if invalid

Usage Examples

This is handled by the gateway and general variable _identityCorrelation.

Example 1: Starting a Process with Identity

// In your gateway or engine service
val identityCorrelation = IdentityCorrelation(
  username = "alice@example.com",
  email = Some("alice@example.com"),
  impersonateProcessValue = Some("department-123")
)

// Start process (two-step flow happens automatically)
val result = processInstanceService.start(
  processKey = "my-process",
  variables = Map("someData" -> "value"),
  identityCorrelation = Some(identityCorrelation)
)

// Result contains processInstanceId with signed correlation

Example 2: Completing a User Task

// In your gateway or engine service
val identityCorrelation = IdentityCorrelation(
  username = "bob@example.com",
  email = Some("bob@example.com")
)

// Complete task (two-step flow happens automatically)
val result = userTaskService.complete(
  taskId = "task-123",
  variables = Map("approved" -> true),
  identityCorrelation = Some(identityCorrelation)
)

// Correlation is signed with processInstanceId and set as variable

Example 3: ServiceWorker with Automatic Verification

// Your ServiceWorker implementation
class MyServiceWorker extends ServiceHandler[MyInput, MyOutput]:

  override def createRequest(input: MyInput)(using context: EngineRunContext): IO[WorkerError, RunnableRequest[MyServiceInput]] =
    // Identity verification happens automatically before this
    // Access the verified identity if needed
    val identity = context.generalVariables._identityCorrelation

    // Create your service request
    ZIO.succeed(RunnableRequest(...))

  // ... rest of implementation

Example 4: Custom EngineConfig with Signing Key

// Your custom context implementation
class ProductionC7Context extends C7Context:

  override def engineConfig: EngineConfig =
    EngineConfig(
      tenantId = Some("production"),
      identitySigningKey = Some(loadSigningKeyFromVault())
    )

  private def loadSigningKeyFromVault(): String =
    // Load from your secret management system
    SecretVault.get("camunda-identity-signing-key")

  // ... other methods

Example 5: Manual Verification in CustomWorker

// If you need strict verification in a CustomWorker
class MyCustomWorker extends CustomHandler[MyInput, MyOutput]:

  override def runWorkZIO(input: MyInput)(using context: EngineRunContext): RunnerOutputZIO =
    for
      // Get correlation from context
      correlation <- ZIO.fromOption(context.generalVariables.identityCorrelation)
                        .orElseFail(WorkerError.UnexpectedError("No identity correlation"))

      // Get processInstanceId
      processInstanceId <- ZIO.fromOption(correlation.processInstanceId)
                              .orElseFail(WorkerError.UnexpectedError("No process instance ID"))

      // Strict verification
      _ <- IdentityVerification.verifySignature(
             correlation,
             processInstanceId,
             context.engineContext.engineConfig.identitySigningKey
           )

      // Continue with your custom logic
      result <- doCustomWork(input, correlation)
    yield result

Troubleshooting

Signature Verification Fails

Symptom: Logs show "Signature verification failed"

Possible Causes: 1. Different signing keys - Gateway and workers using different keys 2. Correlation modified - Someone manually edited the process variables 3. Wrong processInstanceId - Correlation copied from another process

Solution:

# Verify all services use the same key
echo $ORCHESCALA_IDENTITY_SIGNING_KEY

# Check process variables in Camunda
# Ensure identityCorrelation.processInstanceId matches actual process ID

No Signing Key Configured

Symptom: Logs show "No signing key configured"

Possible Causes: 1. Environment variable not set 2. EngineContext not overriding engineConfig

Solution:

# Set environment variable
export ORCHESCALA_IDENTITY_SIGNING_KEY="your-key"

# Or override in EngineContext
class MyContext extends C7Context:
  override def engineConfig: EngineConfig =
    EngineConfig(identitySigningKey = Some("your-key"))

Correlation Not Bound to Process

Symptom: Logs show "IdentityCorrelation present but not bound to a process instance"

Possible Causes: 1. Old process started before signature implementation 2. Correlation created manually without signature

Solution:

Key Rotation Issues

Symptom: Verification fails after key rotation

Possible Causes: 1. Running processes have signatures with old key 2. Workers updated with new key before gateway

Solution:

Migration Guide

As soon you have the gateway running, you have the _identityCorrelation on your process.

Migrating the existing processes means: - Add the Input mapping: _identityCorrelation to the subprocesses that need it.

Technical Details

HMAC-SHA256 Algorithm

The signature is computed as follows:

message = username + email + impersonateProcessValue + issuedAt + processInstanceId
signature = HMAC-SHA256(message, signingKey)
encoded = Base64.encode(signature)

Properties:

Storage Format

The IdentityCorrelation is stored as a JSON object in Camunda process variables:

{
  "username": "alice@example.com",
  "email": "alice@example.com",
  "impersonateProcessValue": "department-123",
  "issuedAt": 1701234567890,
  "processInstanceId": "12345",
  "signature": "dGVzdC1zaWduYXR1cmUtaGVyZQ=="
}

Performance Considerations

Signing:

Verification:

Scalability:

Summary

The Impersonation Solution provides:

Security - Cryptographic binding prevents replay attacks and tampering

Simplicity - Automatic signing and verification, zero-touch for most use cases

Flexibility - Optional vs strict verification, environment variable vs code configuration

Compatibility - Works with both Camunda 7 and Camunda 8

Performance - Minimal overhead, no external dependencies

Maintainability - Clear separation of concerns, well-documented API

Status: ✅ Production Ready

Version: 1.0.0

Last Updated: December 2025