Deepen PracticeOrdered learning track

Authentication in Event-Driven Systems

Learn Java Authentication Pattern - Part 033

Authentication di event-driven systems Java: Kafka producer/consumer identity, broker authentication, event identity propagation, CloudEvents metadata, outbox, audit actor, token anti-pattern, replay defense, Spring Kafka integration, dan failure modes.

10 min read1875 words
PrevNext
Lesson 3340 lesson track23–33 Deepen Practice
#java#authentication#event-driven#kafka+10 more

Part 033 — Authentication in Event-Driven Systems

Target part ini: memahami autentikasi di sistem event-driven: siapa producer-nya, siapa actor bisnisnya, bagaimana identitas dibawa di event, apa yang tidak boleh dibawa, bagaimana consumer memvalidasi event, dan bagaimana audit tetap benar walaupun request asli sudah selesai.

Autentikasi di REST biasanya mudah divisualisasikan:

request masuk -> validasi credential/token -> SecurityContext -> handler

Di event-driven system, event tidak datang sebagai “request user” langsung. Event bisa datang dari broker, queue, stream processor, retry topic, dead-letter topic, CDC connector, scheduler, atau service internal.

Pertanyaannya berubah:

Who produced this event?
Who was the original business actor?
Is the event allowed to represent that actor?
Is this event fresh, replayed, duplicated, or stale?
Can the consumer trust the identity metadata?

Jika pertanyaan ini tidak dijawab, sistem akan scalable tetapi audit dan enforcement-nya rapuh.


1. Core Distinction: Transport Identity vs Business Actor

Dalam event-driven system, minimal ada dua identity berbeda.

IdentityContohDiverifikasi olehDipakai untuk
Transport/client identityKafka principal User:case-service, mTLS subject, SASL usernameBroker / infrastructureApakah producer boleh publish ke topic
Business actor identityactorType=USER, actorId=user-123, tenantId=tenant-aApplication contract + source-of-truthAudit, traceability, downstream policy

Kesalahan paling umum:

Consumer menerima event dengan actorId=user-123.
Consumer menganggap event itu benar karena ada actorId.

Padahal actorId di payload hanyalah data sampai terbukti berasal dari producer yang dipercaya dan topik yang benar.

Mental model aman:

Broker authentication proves producer/client identity.
Event metadata states business actor identity.
Consumer policy decides whether producer is allowed to assert that actor identity.

2. Event Authentication Stack

Event authentication bukan satu check. Ia adalah stack.

Layer-nya:

LayerPertanyaanContoh kontrol
Network/TLSApakah koneksi terenkripsi dan peer valid?TLS, mTLS
Broker authSiapa producer/consumer teknis?SASL/SCRAM, mTLS, OAUTHBEARER
Broker authzClient ini boleh publish/consume topic apa?Kafka ACL, platform RBAC
Event envelopeEvent valid dan lengkap?eventId, source, type, tenantId, actor
SchemaPayload sesuai contract?Avro/JSON Schema/Protobuf
Producer trustSource boleh mengklaim actor ini?allowlist producer-topic-eventType
Business decisionConsumer boleh memproses event ini?domain invariant, policy engine

Kafka mendukung SASL dengan mechanism seperti GSSAPI, PLAIN, SCRAM-SHA-256, SCRAM-SHA-512, dan OAUTHBEARER; SASL dapat berjalan dengan TLS melalui SASL_SSL. Ini menyelesaikan client-to-broker authentication, bukan otomatis menyelesaikan business actor propagation.


3. Event Identity Contract

Event identity harus eksplisit. Jangan berharap consumer menebak dari payload bisnis.

Minimal envelope untuk authentication-aware event:

{
  "specversion": "1.0",
  "id": "evt_01HZP9VKFZ5M8S6B2V0J6C4P8H",
  "source": "urn:service:case-api",
  "type": "reg.case.created.v1",
  "time": "2026-07-03T10:15:30Z",
  "subject": "case/case_123",
  "tenantid": "tenant_a",
  "actor_type": "USER",
  "actor_id": "user_123",
  "actor_session_id": "sess_789",
  "auth_time": "2026-07-03T10:10:12Z",
  "auth_assurance": "aal2",
  "auth_methods": ["password", "totp"],
  "producer_client_id": "case-api",
  "correlation_id": "corr_abc",
  "causation_id": "cmd_xyz",
  "datacontenttype": "application/json",
  "data": {
    "caseId": "case_123",
    "createdBy": "user_123"
  }
}

CloudEvents mendefinisikan core context attributes seperti id, source, type, dan specversion, serta memperbolehkan extension attributes. Untuk authentication metadata, gunakan extension attribute yang jelas dan stabil.

MetadataMakna
sourcesistem yang menghasilkan event
producer_client_idclient teknis yang publish
actor_typesiapa aktor bisnis: USER, SERVICE, SYSTEM, JOB
actor_idid aktor bisnis
tenantidtenant boundary event
auth_timekapan actor diautentikasi
auth_assurancestrength authentication pada saat action
correlation_idtrace user journey
causation_idcommand/event yang menyebabkan event ini

Jangan campur source dengan actor_id. source=case-api tidak berarti actor bisnisnya case-api.


4. Never Put Bearer Tokens in Events

Anti-pattern terbesar:

{
  "eventType": "CaseApproved",
  "accessToken": "eyJhbGciOi...",
  "caseId": "case_123"
}

Kenapa buruk:

  1. Event dapat disimpan lama di log broker.
  2. Token bisa bocor ke consumer yang tidak perlu.
  3. Token expiry tidak cocok dengan event replay.
  4. Refresh/revocation semantics kacau.
  5. Token audience biasanya untuk API tertentu, bukan semua consumer.
  6. Audit menjadi tergantung credential volatile.
  7. DLQ, observability, dan debugging dapat mengekspos token.

Rule:

Event may carry an actor snapshot.
Event must not carry bearer credentials.

Yang boleh dibawa:

{
  "actor_type": "USER",
  "actor_id": "user_123",
  "auth_assurance": "aal2",
  "auth_time": "2026-07-03T10:10:12Z"
}

Yang tidak boleh dibawa:

access token
refresh token
session id raw
password
API key
TOTP secret
recovery code
private key
full cookie header
Authorization header

5. Event Identity Is a Snapshot, Not a Live Login

Saat user melakukan action pukul 10:00, event mungkin diproses pukul 10:05, 12:00, atau replay minggu depan.

Event mewakili fakta:

At time T, actor A caused business event E under authentication context C.

Bukan:

Actor A is still authenticated now.
Use casePakai actor snapshot?Cek state terkini?
Audit logYaTidak wajib
Notification “case created by X”YaTidak wajib
Downstream materialized viewYaKadang
Execute irreversible paymentTidak cukupYa, butuh policy baru
Long-running approval after delayTidak cukupYa, step-up/revalidation mungkin perlu
Regulatory enforcement transitionSnapshot + current invariantYa

Rule:

Event identity is evidence of causation.
It is not a universal authorization grant.

6. Producer-Side Pattern: Capture Auth Context at Command Boundary

Jangan biarkan event builder mengambil SecurityContext dari sembarang thread.

Capture authentication context di boundary command/request, lalu simpan sebagai immutable command context.

Java model

public enum ActorType {
    USER,
    SERVICE,
    SYSTEM,
    JOB
}

public record AuthenticatedActor(
        ActorType actorType,
        String actorId,
        String tenantId,
        String sessionId,
        Instant authTime,
        String assuranceLevel,
        List<String> authenticationMethods,
        String clientId
) {
    public AuthenticatedActor {
        Objects.requireNonNull(actorType, "actorType");
        Objects.requireNonNull(actorId, "actorId");
        Objects.requireNonNull(tenantId, "tenantId");
        Objects.requireNonNull(authTime, "authTime");
        authenticationMethods = List.copyOf(authenticationMethods == null ? List.of() : authenticationMethods);
    }
}

public record EventIdentity(
        String eventId,
        String source,
        String type,
        String subject,
        String tenantId,
        AuthenticatedActor actor,
        String correlationId,
        String causationId,
        Instant occurredAt
) {}

Extract from Spring Security

public final class AuthenticatedActorFactory {

    public AuthenticatedActor from(Authentication authentication, String tenantId) {
        if (authentication == null || !authentication.isAuthenticated()) {
            throw new IllegalStateException("Authenticated request required");
        }

        if (authentication instanceof JwtAuthenticationToken jwtAuth) {
            Jwt jwt = jwtAuth.getToken();
            return new AuthenticatedActor(
                    ActorType.USER,
                    jwt.getSubject(),
                    tenantId,
                    claim(jwt, "sid"),
                    jwt.getIssuedAt() != null ? jwt.getIssuedAt() : Instant.now(),
                    claim(jwt, "acr"),
                    listClaim(jwt, "amr"),
                    claim(jwt, "azp")
            );
        }

        return new AuthenticatedActor(
                ActorType.USER,
                authentication.getName(),
                tenantId,
                null,
                Instant.now(),
                "unknown",
                List.of(authentication.getClass().getSimpleName()),
                null
        );
    }

    private static String claim(Jwt jwt, String name) {
        Object value = jwt.getClaims().get(name);
        return value == null ? null : value.toString();
    }

    @SuppressWarnings("unchecked")
    private static List<String> listClaim(Jwt jwt, String name) {
        Object value = jwt.getClaims().get(name);
        if (value instanceof Collection<?> values) {
            return values.stream().map(Object::toString).toList();
        }
        return List.of();
    }
}

Invariant:

Authentication context is captured once at command boundary.
Outbox relay only publishes already captured event metadata.
Relay must not invent actor identity from its own service account.

7. Outbox Pattern for Identity Preservation

Jika service menyimpan state bisnis dan publish event, gunakan outbox untuk menjaga atomicity.

create table event_outbox (
    outbox_id uuid primary key,
    aggregate_type text not null,
    aggregate_id text not null,
    event_type text not null,
    event_version int not null,
    tenant_id text not null,

    actor_type text not null,
    actor_id text not null,
    actor_session_id text null,
    actor_auth_time timestamptz null,
    actor_assurance text null,
    actor_methods jsonb not null default '[]',
    actor_client_id text null,

    correlation_id text not null,
    causation_id text null,
    payload jsonb not null,
    headers jsonb not null default '{}',

    created_at timestamptz not null default now(),
    published_at timestamptz null,
    publish_attempts int not null default 0
);

create index idx_event_outbox_pending
    on event_outbox(created_at)
    where published_at is null;

Kenapa identity metadata disimpan di outbox?

HTTP request thread punya SecurityContext.
Outbox relay thread tidak punya SecurityContext user.

Jika relay membaca SecurityContextHolder, hasilnya salah atau kosong.

@Transactional
public CaseId createCase(CreateCaseCommand command, AuthenticatedActor actor) {
    CaseEntity saved = caseRepository.save(CaseEntity.newDraft(
            command.caseType(),
            actor.tenantId(),
            actor.actorId()
    ));

    EventIdentity identity = new EventIdentity(
            EventIds.newId(),
            "urn:service:case-api",
            "reg.case.created.v1",
            "case/" + saved.id(),
            actor.tenantId(),
            actor,
            command.correlationId(),
            command.commandId(),
            Instant.now()
    );

    outboxRepository.append(identity, Map.of(
            "caseId", saved.id().toString(),
            "caseType", saved.caseType()
    ));

    return saved.id();
}

8. Kafka Producer Authentication

Kafka producer authentication memastikan client teknis yang publish ke broker adalah client yang dikenal.

MechanismKapan cocokCatatan
mTLSworkload identity berbasis certOperasional cert lifecycle penting
SASL/SCRAMservice credential sederhanaSecret rotation harus rapi
SASL/OAUTHBEARERintegrasi OAuth/IdPToken acquisition + refresh perlu dikelola
Kerberos/GSSAPIenterprise legacy/AD-heavyKompleks, tetapi kuat untuk environment tertentu

Producer config with SASL/SCRAM

Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-1:9093,kafka-2:9093");
props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SASL_SSL");
props.put(SaslConfigs.SASL_MECHANISM, "SCRAM-SHA-512");
props.put(SaslConfigs.SASL_JAAS_CONFIG, """
    org.apache.kafka.common.security.scram.ScramLoginModule required
    username="case-api"
    password="${KAFKA_CASE_API_SECRET}";
    """);
props.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, "/etc/security/kafka/truststore.p12");
props.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, System.getenv("KAFKA_TRUSTSTORE_PASSWORD"));

Production note:

Do not hard-code JAAS secrets in source code.
Prefer secret manager / mounted secret / callback handler.
Rotate producer credentials independently per service.

Topic ACL principle:

case-api may write reg.case.* topics.
case-api must not write reg.payment.* topics.
notification-service may read case events.
notification-service must not write case decision events.

Broker-level authz is not a replacement for event validation, but it reduces blast radius.


9. Event Envelope Validation on Consumer Side

A consumer must not deserialize business payload and execute first.

Correct order:

1. Validate broker client context if available.
2. Validate topic -> event type mapping.
3. Validate envelope fields.
4. Validate schema version.
5. Validate tenant boundary.
6. Validate producer trust policy.
7. Check idempotency/replay.
8. Execute business handler.
public final class EventEnvelopeValidator {

    private final ProducerTrustPolicy producerTrustPolicy;
    private final ProcessedEventRepository processedEvents;

    public void validate(EventEnvelope event, ConsumerContext consumerContext) {
        requireNonBlank(event.id(), "event.id");
        requireNonBlank(event.source(), "event.source");
        requireNonBlank(event.type(), "event.type");
        requireNonBlank(event.tenantId(), "event.tenantId");
        requireNonBlank(event.actor().actorId(), "actor.actorId");
        requireNonBlank(event.correlationId(), "correlationId");

        if (!producerTrustPolicy.canAssert(
                event.source(),
                event.type(),
                event.tenantId(),
                event.actor().actorType())) {
            throw new RejectedEventException("Producer is not trusted for event identity");
        }

        if (processedEvents.alreadyProcessed(event.id())) {
            throw new DuplicateEventException(event.id());
        }
    }

    private static void requireNonBlank(String value, String field) {
        if (value == null || value.isBlank()) {
            throw new RejectedEventException("Missing " + field);
        }
    }
}

Producer trust policy example:

trustedEventSources:
  - source: "urn:service:case-api"
    eventTypes:
      - "reg.case.created.v1"
      - "reg.case.submitted.v1"
    actorTypes:
      - "USER"
      - "SERVICE"
    tenants: "*"

  - source: "urn:service:scheduler"
    eventTypes:
      - "reg.case.sla.expired.v1"
    actorTypes:
      - "SYSTEM"
    tenants: "*"

Invariant:

Not every producer may assert every actor type.

A scheduler may produce system events. It should not claim actor_type=USER unless there is a formally modeled delegated action.


10. Actor Types in Async Systems

Do not force all async events to look user-driven.

Actor typeMeaningExample
USERHuman user caused actionCase officer submits case
SERVICEService account caused actionRisk service recalculates score
SYSTEMPlatform/system rule caused actionSLA timer expired
JOBBatch job caused actionnightly reconciliation
EXTERNAL_PARTNERPartner system caused actionbank sends payment status

Bad event:

{
  "type": "reg.case.autoClosed.v1",
  "actor_id": "admin-user-1"
}

Better event:

{
  "type": "reg.case.autoClosed.v1",
  "actor_type": "SYSTEM",
  "actor_id": "sla-auto-close-policy",
  "causation_id": "timer_123",
  "policy_version": "sla-close-v7"
}

Audit menjadi defensible karena mengatakan apa yang benar-benar terjadi.


11. Tenant Identity in Events

Every authentication-aware event in a multi-tenant system should carry tenant identity explicitly.

{
  "tenantid": "tenant_a",
  "actor_id": "user_123",
  "subject": "case/case_123"
}

Consumer invariant:

event.tenantId must match aggregate tenant boundary.

Do not infer tenant only from topic name. Topic name can help partitioning, but payload/envelope still needs tenant identity for audit, replay, DLQ, and cross-region processing.

if (!event.tenantId().equals(caseEntity.tenantId())) {
    throw new RejectedEventException("Tenant mismatch: event cannot mutate aggregate");
}

12. Idempotency and Replay Defense

Event-driven systems normally deliver at-least-once. Duplicates happen.

create table processed_event (
    consumer_name text not null,
    event_id text not null,
    tenant_id text not null,
    event_type text not null,
    source text not null,
    actor_id text not null,
    processed_at timestamptz not null default now(),
    primary key (consumer_name, event_id)
);
@Transactional
public void handle(EventEnvelope event) {
    validator.validate(event, ConsumerContext.current());

    if (!processedEventRepository.tryMarkProcessing("notification-service", event)) {
        return; // duplicate; safe no-op
    }

    notificationService.sendCaseCreatedNotification(event);
}

Replay defense is not only about attack. It is also about operational correctness.

A replayed PasswordResetRequested or MfaDisabled event can become a security incident if consumer side effects are not idempotent.


13. Event Signing: When Broker Trust Is Not Enough

Broker authentication is enough for many internal systems if producers are controlled, ACLs are strong, topics are private, and event storage is protected.

Event signing becomes useful when:

ScenarioWhy sign?
External partner webhook to event bridgeNeed proof payload came from partner
Cross-organization event exchangeBroker boundary not shared
Long-term audit evidenceNeed tamper-evidence beyond broker ACL
Multi-hop event busNeed source integrity after relay
Untrusted intermediaryNeed end-to-end integrity

Minimal signing model:

signature = HMAC(secret, canonical(event headers + payload hash))

Or asymmetric:

signature = Sign(privateKey, canonical(event headers + payload hash))
consumer verifies with producer public key

Event signing does not eliminate broker auth. It adds source integrity.


14. Spring Kafka: Injecting Authentication Metadata

Producer-side interceptor can add technical headers, but business actor should usually be constructed explicitly by application code.

public final class EventHeaders {
    public static final String EVENT_ID = "ce_id";
    public static final String EVENT_SOURCE = "ce_source";
    public static final String EVENT_TYPE = "ce_type";
    public static final String TENANT_ID = "ce_tenantid";
    public static final String ACTOR_TYPE = "ce_actor_type";
    public static final String ACTOR_ID = "ce_actor_id";
    public static final String CORRELATION_ID = "correlation_id";

    private EventHeaders() {}
}
public ProducerRecord<String, byte[]> toRecord(EventEnvelope event, byte[] payload) {
    ProducerRecord<String, byte[]> record = new ProducerRecord<>(
            topicFor(event.type()),
            event.tenantId() + ":" + event.subject(),
            payload
    );

    add(record, EventHeaders.EVENT_ID, event.id());
    add(record, EventHeaders.EVENT_SOURCE, event.source());
    add(record, EventHeaders.EVENT_TYPE, event.type());
    add(record, EventHeaders.TENANT_ID, event.tenantId());
    add(record, EventHeaders.ACTOR_TYPE, event.actor().actorType().name());
    add(record, EventHeaders.ACTOR_ID, event.actor().actorId());
    add(record, EventHeaders.CORRELATION_ID, event.correlationId());

    return record;
}

Consumer reconstruction:

@KafkaListener(topics = "reg.case-events")
public void onMessage(ConsumerRecord<String, byte[]> record) {
    EventEnvelope envelope = envelopeReader.from(record.headers(), record.value());
    eventEnvelopeValidator.validate(envelope, ConsumerContext.kafka(record));
    caseEventHandler.handle(envelope, record.value());
}

Do not read SecurityContextHolder inside @KafkaListener and expect user identity. There is no HTTP request security context there unless you explicitly created one for internal processing.


15. Mapping Event Actor to SecurityContext: Use Sparingly

Sometimes downstream service code already depends on Authentication.

You may create an internal Authentication from event actor, but it must be explicit and scoped.

public final class EventActorAuthentication extends AbstractAuthenticationToken {
    private final AuthenticatedActor actor;

    public EventActorAuthentication(AuthenticatedActor actor, Collection<GrantedAuthority> authorities) {
        super(authorities);
        this.actor = actor;
        setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return "event-actor-snapshot";
    }

    @Override
    public Object getPrincipal() {
        return actor;
    }

    @Override
    public String getName() {
        return actor.actorId();
    }
}

Scoped execution:

public void withEventSecurityContext(EventEnvelope event, Runnable handler) {
    SecurityContext previous = SecurityContextHolder.getContext();
    SecurityContext context = SecurityContextHolder.createEmptyContext();

    try {
        context.setAuthentication(new EventActorAuthentication(event.actor(), List.of()));
        SecurityContextHolder.setContext(context);
        handler.run();
    } finally {
        SecurityContextHolder.clearContext();
        SecurityContextHolder.setContext(previous);
    }
}

Warning:

An event actor snapshot is not equivalent to a freshly authenticated user.
Do not grant live interactive privileges from it.

Better: pass EventActor explicitly through domain service methods.


16. Authorization in Consumers

Authentication metadata answers “who/what caused this event?”

Consumer authorization asks:

Is this consumer allowed to process this event and mutate this aggregate?

Do not assume upstream authorization is always enough.

public void apply(CaseApproved event) {
    CaseRecord caseRecord = caseRepository.require(event.caseId());

    if (!caseRecord.tenantId().equals(event.tenantId())) {
        throw new RejectedEventException("Cross-tenant event rejected");
    }

    if (!caseRecord.isSubmitted()) {
        throw new RejectedEventException("Cannot approve non-submitted case");
    }

    // proceed
}

17. Long-Running Workflows and Authentication Drift

Event-driven workflows can outlive sessions, roles, and even employment.

Day 1: user submits enforcement case.
Day 3: risk scoring completes.
Day 7: decision workflow auto-escalates.
Day 10: user account is disabled.

Which actor should be in audit?

EventActor
case.submittedoriginal user
risk.scoredrisk service
case.auto_escalatedescalation policy/system
case.final_decision_submittedcurrent decision maker

Do not propagate the original user forever as if they are doing every step.

Audit should say:

The user caused submission.
The risk service caused scoring.
The policy caused escalation.

18. Dead Letter Queue Security

DLQ often leaks sensitive metadata.

Rules:

  1. Do not put credentials in event.
  2. Redact sensitive payload fields before DLQ if possible.
  3. Restrict DLQ consumers more tightly than normal topic consumers.
  4. Treat DLQ replay as privileged operation.
  5. Include rejection reason without dumping secrets.
  6. Log event id, source, type, tenant, actor id, not full token/header.

DLQ replay must preserve original identity metadata.

Replay actor = operator who initiated replay.
Original actor = actor in original event.

Represent both:

{
  "original_actor_id": "user_123",
  "replay_actor_id": "ops_456",
  "replay_reason": "fixed schema mapping bug",
  "replay_time": "2026-07-03T12:00:00Z"
}

19. Observability

Log these fields for every accepted/rejected event:

FieldPurpose
event_ididempotency, replay trace
event_typepolicy and debugging
sourceproducer trust
tenant_idisolation
actor_typeaudit semantics
actor_idcausation
consumerprocessing owner
topic / partition / offsetbroker trace
correlation_iduser journey
rejection_reasondetection

Metrics:

events.accepted.count{type,source,tenant}
events.rejected.count{reason,type,source}
events.duplicate.count{consumer,type}
events.cross_tenant_rejected.count{source,type}
events.missing_actor.count{source,type}
events.dlq.count{reason,type}

Alert examples:

source not in allowlist starts producing events
sudden spike in cross-tenant rejection
event type appears on unexpected topic
missing actor metadata after deployment
DLQ contains security-sensitive event type

20. Testing Strategy

Unit tests:

missing actor -> reject
missing tenant -> reject
unknown source -> reject
source cannot assert USER actor -> reject
duplicate event id -> no-op
cross-tenant aggregate mutation -> reject

Contract tests:

event type
schema version
required identity metadata
tenant metadata
auth assurance representation
correlation/causation id

Security regression fixture:

{
  "id": "evt_attack_1",
  "source": "urn:service:notification-service",
  "type": "reg.case.approved.v1",
  "tenantid": "tenant_b",
  "actor_type": "USER",
  "actor_id": "admin-user"
}

Expected:

Rejected because notification-service cannot assert case.approved.
Rejected because aggregate tenant mismatch.

Replay tests:

same event twice -> side effect once
same event after consumer restart -> side effect once
DLQ replay -> original actor preserved + replay operator recorded

21. Failure Modes

Failure modeCauseImpactControl
Token-in-event leakBearer token serialized in payload/headerCredential compromiseNever put credentials in events; redaction
Actor spoofingConsumer trusts actorId blindlyAudit fraud, privilege abuseProducer trust policy
Cross-tenant replayEvent from tenant A mutates tenant BData breachTenant invariant check
SecurityContext misuseKafka listener reads stale ThreadLocalWrong principalExplicit actor context; cleanup
Relay actor overwriteOutbox relay uses service account as actorAudit lossStore actor in outbox
Infinite user propagationLong workflow keeps original user for all actionsFalse auditModel SERVICE/SYSTEM actors
Broker ACL too broadAny service writes any topicEvent injectionLeast privilege ACL
DLQ overexposureDLQ accessible widelySensitive data leakDLQ access control/redaction
Replay side effectDuplicate event sends emails/payment twiceOperational/security damageIdempotency table
Stale assurance misuseOld aal2 event used for new sensitive actionPolicy bypassRevalidate for sensitive action

22. Production Checklist

[ ] Producer identity is authenticated at broker or gateway boundary.
[ ] Producer is authorized only for required topics.
[ ] Event envelope includes event id, source, type, time, tenant, actor, correlation id.
[ ] Bearer tokens, sessions, cookies, API keys, and secrets are never placed in events.
[ ] Consumer validates producer trust policy before business handling.
[ ] Consumer validates tenant boundary before aggregate mutation.
[ ] Event idempotency is implemented per consumer.
[ ] DLQ replay preserves original actor and records replay operator.
[ ] Long-running workflows model USER, SERVICE, SYSTEM, and JOB actors explicitly.
[ ] Audit logs include event id, source, actor, tenant, consumer, topic, partition, offset.
[ ] Security tests include spoofed actor, wrong source, wrong tenant, duplicate event.
[ ] Observability alerts detect unknown source, missing metadata, cross-tenant rejection.

23. Mental Model Summary

Event-driven authentication is not “validate JWT in consumer”.

The correct model:

Broker authenticates technical producer.
Envelope carries business actor snapshot.
Consumer validates source, tenant, event type, and producer trust.
Business handler enforces domain invariants.
Audit records causation without pretending the original session still exists.

The hardest part is keeping identities separate:

producer identity != actor identity
actor snapshot != live login
event source != tenant
replay operator != original actor
service action != user action

If you preserve those distinctions, event-driven authentication becomes defensible.


References

Lesson Recap

You just completed lesson 33 in deepen practice. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.