Pass spring boot SecurityContext to workflow

I was able to pass MDC values by using temporal ContextPropagator. Is there a way to pass SecurityContext to workflow and activities? If not would you recommend passing it as workflow or activity input?

import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;

final SecurityContext context = SecurityContextHolder.getContext();

Whats use case where you need this in workflow code? For activities you should be able to autowire the security context as activity impl can be Component (see sample here if it helps).

final SecurityContext context = SecurityContextHolder.getContext();

This code is already inside Spring class annotated with @Service and this service is being called by activity. But context value is null. Only reason I can think of is that context is not propagated to activities. This also happens if we use @Async or create a new thread in which we can this service class. To resolve that we need to delegate security context. See https://www.baeldung.com/spring-security-async-principal-propagation .

Do we have similar thing for temporal by which we can delegate it to activies

Are these workflows long-running? Does this context expire? Is it OK to persist this context?

Here is scenario
User makes API request → Start workflow → Start activity → Calls spring service class → update/insert db records

In our current logic while updating db record we have security context by which we get information about user who made request (Decoded JWT) e.g email, roles, name etc. Now while persisting we also persist user information corresponding to each record which we update or insert (For Audit)

This works well as of now without using temporal by delegating context and using spring async methods. Since now I have moved workflow to temporal this context is null.

I see. You need to find a way to inject the security context from the ContextPropagator into the workflow or activity threads.

I tried to Serialize SecurityContext in ContextPropagator which was fine but deserialization had some issues with

DataConverter.getDefaultInstance()
              .fromPayload(entry.getValue(), SecurityContextImpl.class, SecurityContextImpl.class)

As inbuilt authentication does not have default constructor. I think it is achievable just need to refactor some code. But for now I am using MDC as I just need userId from security context. This was easy fix for me.

You don’t have to use Temporal DataConverter to serialize/deserialize header fields. How is the SecurityContext serialized for HTTP/gRPC calls sent over the wire?

Not sure what do you mean by I don’t have to use DataConvertor. This is what I have in ContextPropagator



import io.temporal.api.common.v1.Payload;
import io.temporal.common.context.ContextPropagator;
import io.temporal.common.converter.DataConverter;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.MDC;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;

public class TemporalMdcContextPropagator implements ContextPropagator {

  private static final String MDC_KEY = "MDC";
  private static final String SECURITY_CONTEXT_KEY = "SECURITY-CONTEXT";

  @Override
  public String getName() {
    return this.getClass().getName();
  }

  @Override
  public Object getCurrentContext() {
    final Map<String, Object> contextMap = new HashMap<>();
    contextMap.put(MDC_KEY, MDC.getCopyOfContextMap());
    contextMap.put(SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext());
    return contextMap;
  }

  @Override
  public void setCurrentContext(Object context) {
    Map<String, Object> contextMap = (Map<String, Object>) context;
    SecurityContextHolder.setContext((SecurityContextImpl) contextMap.get(SECURITY_CONTEXT_KEY));
    MDC.setContextMap((Map<String, String>)contextMap.get(MDC_KEY));
  }

  @Override
  public Map<String, Payload> serializeContext(Object context) {
    Map<String, Object> contextMap = (Map<String, Object>) context;
    Map<String, Payload> serializedContext = new HashMap<>();
    for (Map.Entry<String, Object> entry : contextMap.entrySet()) {
      serializedContext.put(
          entry.getKey(), DataConverter.getDefaultInstance().toPayload(entry.getValue()).get());
    }
    return serializedContext;
  }

  @Override
  public Object deserializeContext(Map<String, Payload> context) {
    Map<String, Object> contextMap = new HashMap<>();
    for (Map.Entry<String, Payload> entry : context.entrySet()) {
      Class cls = MDC_KEY.equals(entry.getKey()) ? Map.class : SecurityContextImpl.class;
      contextMap.put(
          entry.getKey(),
          DataConverter.getDefaultInstance()
              .fromPayload(entry.getValue(), cls, cls));
    }
    return contextMap;
  }
}

And I am getting following error

Caused by: io.temporal.common.converter.DataConverterException: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.springframework.security.core.Authentication` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
 at [Source: (byte[])"{"authentication":{"authorities":[{"role":"READ_ROLE","authority":"READ_ROLE"},{"role":"MANAGE_USER","authority":"MANAGE_CUSTOMER"},{"role":"CREATE_CONFIG","authority":"CREATE_CONFIG"},CREATE_"[truncated 17700 bytes]; line: 1, column: 19] (through reference chain: org.springframework.security.core.context.SecurityContextImpl["authentication"])
	at io.temporal.common.converter.JacksonJsonPayloadConverter.fromData(JacksonJsonPayloadConverter.java:101) ~[temporal-sdk-1.19.1.jar:na]
	at io.temporal.common.converter.PayloadAndFailureDataConverter.fromPayload(PayloadAndFailureDataConverter.java:95) ~[temporal-sdk-1.19.1.jar:na]
	at io.temporal.common.converter.DefaultDataConverter.fromPayload(DefaultDataConverter.java:33) ~[temporal-sdk-1.19.1.jar:na]

You decided to use the DataConverter to serialize context. It looks like SecurityContextImpl is not serializable using it. I would use another way to convert SecrityContext to bytes.

ContextPropagator Interface

  Map<String, Payload> serializeContext(Object context);

  /** Turn the serialized header data into context object(s) */
  Object deserializeContext(Map<String, Payload> header);

Accepts and returns Payload.
Are you saying first I should serialize using bytes and then convert it to Map<String, Payload> or Payload to bytes for deserialize

Just checked older version of temporal-sdk was using bytes in ContextPropagator
https://javadoc.io/doc/io.temporal/temporal-sdk/1.3.0/io/temporal/common/context/ContextPropagator.html

Here is how OpenTracingInterceptor does this.

Thanks for pointer

Below code is working for me and setting correct security context

import com.google.common.reflect.TypeToken;
import io.temporal.api.common.v1.Payload;
import io.temporal.common.context.ContextPropagator;
import io.temporal.common.converter.GlobalDataConverter;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang3.SerializationUtils;
import org.slf4j.MDC;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;

public class TemporalMdcContextPropagator implements ContextPropagator {

  private static final Type HASH_MAP_STRING_STRING_TYPE =
          new TypeToken<HashMap<String, String>>() {}.getType();
  private static final String MDC_KEY = "MDC";
  private static final String SECURITY_CONTEXT_KEY = "SECURITY-CONTEXT";

  @Override
  public String getName() {
    return this.getClass().getName();
  }

  @Override
  public Object getCurrentContext() {
    final Map<String, Object> contextMap = new HashMap<>();
    contextMap.put(MDC_KEY, MDC.getCopyOfContextMap());
    contextMap.put(SECURITY_CONTEXT_KEY, SerializationUtils.serialize(SecurityContextHolder.getContext()));
    return contextMap;
  }

  @Override
  public void setCurrentContext(Object context) {
    final Map<String, Object> contextMap = (Map<String, Object>) context;
    final SecurityContextImpl securityContext = SerializationUtils.deserialize((byte[]) contextMap.get(SECURITY_CONTEXT_KEY));
    SecurityContextHolder.setContext(securityContext);
    MDC.setContextMap((Map<String, String>)contextMap.get(MDC_KEY));
  }

  @Override
  public Map<String, Payload> serializeContext(Object context) {
    Map<String, Object> contextMap = (Map<String, Object>) context;
    Map<String, Payload> serializedContext = new HashMap<>();
    for (Map.Entry<String, Object> entry : contextMap.entrySet()) {
      serializedContext.put(entry.getKey(), GlobalDataConverter.get()
              .toPayload(entry.getValue()).get());
    }
    return serializedContext;
  }

  @Override
  public Object deserializeContext(Map<String, Payload> context) {
    Map<String, Object> contextMap = new HashMap<>();
    for (Map.Entry<String, Payload> entry : context.entrySet()) {
      if (MDC_KEY.equals(entry.getKey())) {
        contextMap.put(
                entry.getKey(),
                GlobalDataConverter.get()
                        .fromPayload(entry.getValue(), HashMap.class, HASH_MAP_STRING_STRING_TYPE));
      } else {
        final byte[] bytes = entry.getValue().getData().toByteArray();
        contextMap.put(entry.getKey(),  bytes);
      }

    }
    return contextMap;
  }
}