It is tempting to treat user authentication sessions as ambient context. You pass them through API layers, into service calls, across event processors, and through background jobs. If a component needs to know who the user is, it needs the user's session, right?
This conflates authentication with context. You're treating an external trust artifact as internal state, and the result is coupling, performance waste, and systems that break in asynchronous scenarios, event-driven architectures, and integration patterns.
Sessions Validate at Boundaries, Context Flows Internally
Authentication sessions serve one purpose: validating identity at a security boundary. Once validated, the session should be consumed and replaced with explicit context and service credentials.
A user session arrives at the boundary: an API endpoint, gateway, or edge function. The boundary validates the session and confirms identity. The session is then consumed. You extract only the necessary context: user_id, tenant_id, correlation_id, relevant metadata. From that point forward, internal operations use service roles. Components operate with service credentials like database connections, API keys, and queue permissions. User context is passed explicitly as structured data, not as the original session token.
The session's job ends at the boundary. Everything beyond that point operates within your system's trust domain using service-level credentials and explicit context propagation.
Why Sessions Don't Belong in Internal Flows
Sessions are external trust artifacts. A user session token represents authentication with an external identity provider or your auth system. It's proof that someone outside your system is who they claim to be. Once verified at your boundary, propagating it further treats authentication as global state rather than a specific validation event.
Repeated validation is wasteful. Every component that receives a user session must validate it: verify cryptographic signatures, check expiration, potentially query the auth provider. If your request flows through multiple services or processors, you're performing this validation repeatedly for a single user action.
Coupling to external authentication. When internal components accept user sessions, they become coupled to the token format, the issuer's validation requirements, token refresh logic, and external auth provider availability. Changing auth providers or migrating token formats requires updating every component that parses sessions.
When Session Propagation Fails
Propagating user sessions through internal components creates predictable failure patterns regardless of your architecture.
Session expiration mid-flow happens constantly. Event processors run minutes later and the session has expired. Background jobs run hours later and the session is gone. Async processors retry and the session is invalid. You build workarounds: token refresh in queues, session persistence in metadata, special "system sessions" for background work.
Missing sessions entirely breaks your logic. Scheduled reports run at 3 AM with no user logged in. Webhooks arrive from external systems with no user session. System maintenance tasks have no user context. You duplicate logic: one path for user requests and one for non-user operations.
Context across time and actors fragments workflows. Multi-step workflows span hours. The original user's session expires. Different actors have different sessions. Background processors have none. The workflow breaks without explicit context that persists beyond the session lifetime.
Boundaries validate external trust artifacts and convert them to internal context. The API gateway validates the user session, extracts explicit context like user_id, tenant_id, and correlation_id, and passes that forward instead of the session. Each component uses service credentials for technical capability (database connections, API keys, queue permissions) and performs explicit authorization checks using the context. This works identically whether triggered by user requests, webhooks, scheduled jobs, or system events.
Common Objections
"We Lose User Context for Auditing"
You lose the session token, not the user context. Proper auditing requires intentional context capture: user_id, tenant_id, correlation_id, source_ip, user_agent, and timestamp.
This context flows explicitly through every component. Relying on session tokens for auditing is worse because sessions don't persist for audit trails, you're logging authentication artifacts instead of business context, and different components might parse tokens differently.
Intentional data flow is the point. If you need user_id in audit logs, require it as an explicit parameter. This forces you to think about what context crosses boundaries and prevents accidental omissions.
"Compliance Requires User Identity Throughout the System"
Compliance frameworks (SOC2, HIPAA, GDPR) require knowing who performed an action and when. User sessions are for authentication, proving identity at the boundary. Passing user_id in explicit context satisfies compliance. Passing the session token does not improve compliance. It couples your compliance logging to your auth mechanism.
"Service Roles Create Privilege Escalation Risks"
This assumes your service role can do things it shouldn't based on improper input validation or authorization bypasses. That's a security bug, not a pattern problem.
Service roles provide technical capability like database access and API credentials. Business logic enforces authorization through grant checks, tenant isolation, and permission verification. This authorization check is necessary whether you use service roles or propagate user sessions.
Service roles make this clearer. The service has technical capability but requires explicit business authorization. User sessions blur this distinction by suggesting the session itself grants permission.
"We Need Sessions for Distributed Tracing"
You need correlation IDs for distributed tracing, not session tokens.
Distributed systems require explicit tracing infrastructure: correlation_id, trace_id, span_id, and timestamps. This works for all request types including user requests, background jobs, webhooks, internal system tasks, lambda invocations, and event processors.
If you rely on session tokens for tracing, non-user-initiated requests fail. Correlation IDs are the intentional, universal solution.
"We're Using a Monolith, This Doesn't Apply"
Using a monolith does not exempt you from these principles. You still must avoid global user session context and pass user context within structures appropriate to the needs of each module.
Treating the session as globally accessible state makes distribution impossible when you eventually need to scale. It makes unit testing a nightmare because every test requires mocking session context. It couples your service layer to your web layer, preventing reuse from background jobs, CLI tools, or internal scripts.
The same principles apply: modules accept explicit context, operate with their own credentials, and perform explicit authorization. Whether those modules are in separate processes or the same codebase is irrelevant to the design.
Respect the Boundary
User authentication sessions serve one purpose: validating identity at a security boundary. Once validated, the session has done its job. Extract the necessary context, discard the session, and operate with service roles internally.
Propagating sessions through internal flows treats authentication as global state, couples components to auth mechanisms, wastes resources on redundant validation, and breaks in event-driven architectures, background jobs, approval workflows, webhook integrations, and async processing.
This isn't about microservices versus monoliths. It's about recognizing architectural boundaries and respecting them. Pass the context you need, not the artifact you don't. Components shouldn't depend on external auth formats. Authentication artifacts belong at boundaries, not in internal flows.
Top comments (0)