Skip to content

feat: Adds opt-in X-User-Id header to responses#22689

Open
jason-p-pickering wants to merge 25 commits into
masterfrom
session-id-header
Open

feat: Adds opt-in X-User-Id header to responses#22689
jason-p-pickering wants to merge 25 commits into
masterfrom
session-id-header

Conversation

@jason-p-pickering

@jason-p-pickering jason-p-pickering commented Jan 4, 2026

Copy link
Copy Markdown
Contributor

Adds two opt-in response headers for reverse-proxy request/user correlation.

Background

Correlating user actions with requests is an ongoing challenge for system administrators investigating performance issues: which user is responsible, how is their account configured, which client are they using? There is currently no easy way to answer this at the network boundary. By emitting an encoded user identifier in the response, a reverse proxy can capture it in its access log and correlate requests back to a DHIS2 user for diagnostic purposes.

What this adds

UserIdFilter — emits an X-User-ID response header containing the authenticated user's UID encrypted with AES-256-GCM. Requires configuration (off by default).

SessionIdFilter — extended to optionally emit an X-Session-ID response header containing the same SHA-256-hashed session ID already written to MDC (%X{sessionId}). The MDC is always populated for authenticated requests regardless of this setting; the header is opt-in.

Configuration

Emit encrypted user UID as X-User-ID response header (default: off)
logging.user_id_header.enabled = on

Base64-encoded 32-byte AES-256 key (generate with: openssl rand -base64 32)
logging.user_id_encryption_key =

Emit hashed session ID as X-Session-ID response header (default: off)
logging.session_id_header.enabled = on

Algorithm details

X-User-ID: AES-256-GCM, random 12-byte IV per encryption, output format v1.<base64url(IV + ciphertext)>. The value is cached in-JVM for 5 minutes to avoid re-encrypting on every request. Anyone with access to the proxy logs and the encryption key can recover the user UID. The key should obviously be kept safe.

X-Session-ID: SHA-256 hash of the session ID (base64-encoded), same value as MDC sessionId. Useful for correlating proxy log lines with DHIS2 log lines, but not reversible.

@jason-p-pickering
jason-p-pickering marked this pull request as draft January 4, 2026 16:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an opt-in session identification mechanism for reverse proxy request correlation by adding an encrypted X-Session-ID response header for authenticated users.

  • Implements SessionIdHeaderFilter that encrypts userUid:hash(sessionId) using AES-GCM
  • Adds two configuration keys: logging.session_id_header.enabled (default off) and logging.session_id_encryption_key
  • Includes integration test that validates encryption/decryption roundtrip

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
SessionIdHeaderFilter.java New servlet filter that encrypts and adds X-Session-ID header to responses for authenticated users with active sessions
SessionIdHeaderFilterTest.java Integration test that validates the filter encrypts user UID and session hash correctly
DhisWebApiWebAppInitializer.java Registers the new sessionIdHeaderFilter in the servlet filter chain
ConfigurationKey.java Adds two configuration keys for enabling the feature and storing the encryption secret
HttpClientAdapter.java Adds convenience method getHeader() to access response headers in tests

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@teleivo teleivo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this different to the hashed version of the Session ID we put into the Mapped Diagnostic Context? Ideally we put this value into the MDC in one place (filter) and use it in logging and elsewhere. We do this for RequestId for example.

Logging should not need a config in the dhis.conf IMHO. Users can configure logging via log4j2.xml and its MDC support like so request_id=%X{xRequestID}.

One detail on the current session id logging: the config name is misleading

* Adds a hashed (SHA-256) session_id to each log line. Useful for tracking which user is
* responsible for the logging line.
*/
LOGGING_REQUEST_ID_ENABLED("logging.request_id.enabled", Constants.ON, false),

@jason-p-pickering jason-p-pickering changed the title feat: Adds opt-in X-Session-Id header to responses feat: Adds opt-in X-User-Id header to responses Jan 5, 2026
@sonarqubecloud

sonarqubecloud Bot commented Feb 3, 2026

Copy link
Copy Markdown

jason-p-pickering and others added 2 commits May 11, 2026 07:28
- SessionIdFilter now always populates MDC for authenticated requests;
  logging output is controlled via log4j2 pattern layout, not dhis.conf.
  Reviewer: 'Logging should not need a config in the dhis.conf IMHO.'
- X-Session-ID header emission remains opt-in via
  logging.session_id_header.enabled (reverse proxy boundary feature).
- Remove the now-unused logging.session_id ConfigurationKey.
- Fix broken merge state: restore missing excludableShallowEtagHeaderFilter
  registration in DhisWebApiWebAppInitializer.
- Update SessionIdFilterTest to capture MDC during filter chain execution
  (MDC is correctly cleared in the finally block).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Conflict resolutions:
- ConfigurationKey.java: drop LOGGING_QUERY_METHOD (removed from master in
  #23040), keep PR additions (LOGGING_SESSION_ID_HEADER_ENABLED,
  LOGGING_USER_ID_HEADER_ENABLED, LOGGING_USER_ID_ENCRYPTION_KEY)
- DhisWebApiWebAppInitializer.java: drop AppHtmlNoCacheFilter registration
  (not in current master), keep userIdHeaderFilter (PR feature)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jason-p-pickering

jason-p-pickering commented May 11, 2026

Copy link
Copy Markdown
Contributor Author

How is this different to the hashed version of the Session ID we put into the Mapped Diagnostic Context? Ideally we put this value into the MDC in one place (filter) and use it in logging and elsewhere. We do this for RequestId for example.

Logging should not need a config in the dhis.conf IMHO. Users can configure logging via log4j2.xml and its MDC support like so request_id=%X{xRequestID}.

One detail on the current session id logging: the config name is misleading

* Adds a hashed (SHA-256) session_id to each log line. Useful for tracking which user is
* responsible for the logging line.
*/
LOGGING_REQUEST_ID_ENABLED("logging.request_id.enabled", Constants.ON, false),

Thanks for the review. Both points addressed in the last push.

SessionIdFilter now always populates the sessionId MDC key for authenticated requests (same as RequestIdFilter does for xRequestID). The same hashed value is then optionally emitted as the X-Session-ID response header.

On logging config in dhis.conf: Agreed. The logging.request_id.enabled / logging.session_id config gate on MDC has been removed. MDC is always populated; users control whether it appears in log output via their log4j2 pattern layout (%X{sessionId}). The only dhis.conf knob remaining is logging.session_id_header.enabled, which controls the reverse-proxy header. That has nothing to do with logging per se and requires opt-in in the config.

On the misleading config name: logging.request_id.enabled had already been renamed to logging.session_id on master. Since the MDC gate is now gone entirely, we've removed that key from ConfigurationKey altogether.

To be explicit about the two features and why both exist:

  • X-Session-ID: SHA-256 hash of the session ID, emitted as a response header. Not reversible, but lets a reverse proxy correlate its access log entries with DHIS2 log lines that carry the same sessionId MDC value.
  • X-User-ID : AES-GCM encrypted user UID, emitted as a response header. Reversible with the configured key, so a reverse proxy can identify which user is responsible for a given request.

@jason-p-pickering
jason-p-pickering requested a review from teleivo May 11, 2026 06:11
@jason-p-pickering
jason-p-pickering marked this pull request as ready for review May 11, 2026 06:12
@sonarqubecloud

Copy link
Copy Markdown

@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants