Skip to content

feat(auth): publish authenticated principal metadata - #1011

Open
nerdalert wants to merge 1 commit into
praxis-proxy:mainfrom
nerdalert:pr-ready/praxis
Open

feat(auth): publish authenticated principal metadata#1011
nerdalert wants to merge 1 commit into
praxis-proxy:mainfrom
nerdalert:pr-ready/praxis

Conversation

@nerdalert

@nerdalert nerdalert commented Aug 20, 2026

Copy link
Copy Markdown
Member

Summary

This PR adds the common authenticated-principal handoff needed by downstream Praxis filters.

After Basic Auth credentials are successfully verified, the filter publishes the request-scoped metadata key:

identity.user_id

The value contains the verified username. Downstream filters can use it for quota enforcement, authorization, auditing, and other identity-aware behavior without reparsing credentials.

Identity is published only after successful verification. Missing, malformed, unknown, or incorrect credentials publish no identity.

Metadata publication is now fallible. If the request metadata capacity is exhausted, authentication fails closed with a 401 response instead of returning Continue without the promised identity.

Usernames are bounded to 256 bytes. Inline credentials are rejected during configuration parsing when they exceed that limit; KV-backed identities retain runtime protection.

Why this belongs in Praxis core

Authentication and identity establishment are core request-pipeline responsibilities.

Downstream filters should consume a trusted identity established by the authentication layer rather than:

  • reparsing the Authorization header;
  • duplicating Basic Auth logic;
  • trusting a client-controlled identity header;
  • coupling quota enforcement to credential storage.

The reusable contract is:

  authentication filter
      -> verified identity.user_id metadata
      -> downstream filters

Basic Auth is the first producer of this metadata. Future authentication mechanisms such as OIDC, API keys, mTLS, or external authentication can publish the same key after successful verification.

This is required by the Grid-aware token-rate-limit work, where Praxis AI must establish the authenticated principal before reserving quota.

Related work:

The AI PR is the provider-selection/load-balancing foundation. The distributed quota behavior is demonstrated separately by the experimental demo.

Implementation details

  • Adds the shared IDENTITY_USER_ID_METADATA constant.
  • Adds fallible try_set_metadata.
  • Keeps the existing set_metadata compatibility API for filters that intentionally tolerate rejected metadata writes.
  • Publishes identity only after successful Basic Auth verification.
  • Rejects authentication when trusted identity publication fails.
  • Validates the 256-byte username boundary.
  • Preserves configurable Authorization-header stripping.
  • Adds focused Basic Auth and metadata-capacity tests.

No passwords, Authorization headers, credential-store values, or other credential material are placed in request metadata.

Performance considerations

This adds no network calls, filesystem access, Kubernetes access, or remote lookup to the request path.

The successful-authentication path performs:

  1. the existing credential verification;
  2. a bounded username-length check;
  3. one request-scoped metadata insertion.

The metadata operation is bounded and does not introduce a new lock, cache, background task, or external dependency.

When metadata capacity is exhausted, the request fails closed immediately. This prevents downstream filters from running without the identity they require.

No benchmark is included because this change does not add a new network or synchronization boundary. Performance benchmarking remains part of the broader AI token-rate-limit qualification.

Security considerations

The identity metadata is trusted only because it is written after credential verification.

Clients must not be allowed to set or override identity.user_id directly.

The metadata contains only the verified username. It does not contain:

  • the password;
  • the Authorization header;
  • encoded credentials;
  • credential-store data;
  • session secrets.

Production telemetry should not use raw identity values as unbounded metric labels.

Validation

  • cargo test -p praxis-proxy-filter --features basic-auth-filter basic_auth --locked

    • 39 passed, 0 failed
  • cargo test -p praxis-proxy-filter context --locked

    • 67 passed, 0 failed
  • git diff --check

    • passed

Coverage includes:

  • successful identity publication;
  • missing credentials;
  • malformed credentials;
  • invalid credentials;
  • unknown users;
  • Authorization-header stripping;
  • 256-byte username acceptance;
  • 257-byte username rejection;
  • metadata-capacity failure;
  • failed authentication without identity metadata.

Breaking changes

None expected for existing valid configurations.

The new 256-byte username limit is an explicit validation constraint required to keep request metadata bounded. Inline configurations exceeding this limit now fail at startup. KV- backed usernames exceeding the limit are rejected at request time.

Downstream filters that consume identity.user_id should be placed after the authentication filter.

@nerdalert
nerdalert requested a review from a team August 20, 2026 04:10
@nerdalert
nerdalert requested a review from shaneutt as a code owner August 20, 2026 04:10
@nerdalert
nerdalert force-pushed the pr-ready/praxis branch 2 times, most recently from 3d26ded to 4ca6d53 Compare August 20, 2026 04:20
Signed-off-by: Brent Salisbury <bsalisbu@redhat.com>

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

PR Review

Purpose: Publishes identity.user_id metadata after successful Basic Auth verification, enabling downstream filters to consume a trusted authenticated principal without reparsing credentials.

Assessment: Well-structured change with clear security intent. The fail-closed behavior on metadata capacity exhaustion and the bounded username length are sound design choices. Three improvements needed.

Severity Count Summary
Medium 3 Missing Error trait on public type, non-idiomatic result discard, test coverage gap for KV-backed username rejection

Comment thread filter/src/context.rs
const MAX_METADATA_ENTRIES: usize = 128;

/// Failure returned when a request metadata value cannot be stored.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Medium] MetadataError is publicly exported and used as the error position in Result<(), MetadataError>, but it doesn't implement Display or std::error::Error. Project conventions require thiserror for error types. Without these traits, callers can't use ? ergonomically or compose with standard error-handling crates.

#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
pub enum MetadataError {
    #[error("metadata key is empty or exceeds the 64-byte limit")]
    InvalidKey,
    #[error("metadata value exceeds the 256-byte limit")]
    ValueTooLong,
    #[error("request metadata entry limit reached")]
    Capacity,
}

Comment thread filter/src/context.rs
/// memory growth.
pub fn set_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
match self.try_set_metadata(key, value) {
Ok(()) | Err(_) => {},

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Medium] Non-idiomatic way to discard a Result. The match with Ok(()) | Err(_) => {} is functionally correct but unusual -- both arms do the same thing, so the exhaustive matching doesn't convey intent better than the standard idiom:

let _ = self.try_set_metadata(key, value);

// The authenticated principal is trusted only after verification.
// Keep it bounded because filter metadata is request-scoped and can be
// consumed by downstream admission and routing filters.
if username.len() > 256 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Medium] This runtime 256-byte username check exists specifically for the KV-backed credential path (inline usernames are caught at config time by the TryFrom validation), but no test exercises a KV-backed credential with a >256-byte username being rejected at request time. The path is untested.

Add a test that:

  1. Creates a filter with kv_store source
  2. Stores a credential with a 257-byte username key in the KV store
  3. Sends a request with matching 257-byte base64-encoded credentials
  4. Asserts the request is rejected with 401 and no identity.user_id metadata is published

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.

2 participants