feat(auth): publish authenticated principal metadata - #1011
Conversation
3d26ded to
4ca6d53
Compare
Signed-off-by: Brent Salisbury <bsalisbu@redhat.com>
4ca6d53 to
c2fe248
Compare
praxis-bot
left a comment
There was a problem hiding this comment.
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 |
Signed-off-by: Brent Salisbury <bsalisbu@redhat.com>
leseb
left a comment
There was a problem hiding this comment.
A side note, I see this as a prerequisite for making policy-engine production-supported and enabled by default. Once PPE uses Praxis’s transport and ships the outstanding security fixes, we can consider removing the development-only basic_auth filter and standardizing on JWT/external-IdP authentication. The removal should remain a separate follow-up.
@shaneutt wdyt?
|
Agreed. 👍 |
shaneutt
left a comment
There was a problem hiding this comment.
Merge conflicts and comments need resolution. Keep everything contained to the filter, since we will be removing it eventually anyhow. Once everything is resolved, LGTM.
| /// insert thousands of unique keys per request. | ||
| const MAX_METADATA_ENTRIES: usize = 128; | ||
|
|
||
| /// Failure returned when a request metadata value cannot be stored. |
There was a problem hiding this comment.
| /// Failure returned when a request metadata value cannot be stored. | |
| /// Failure returned when request metadata values cannot be stored. |
| /// This is the fallible form for filters whose security or routing | ||
| /// contract requires the metadata to be present before continuing. |
There was a problem hiding this comment.
Should include a written example of when this would be the case.
| /// Trusted metadata key for the authenticated principal identifier. | ||
| /// | ||
| /// Authentication filters write this key only after successfully verifying | ||
| /// credentials. Downstream filters may use it as a generic identity contract; | ||
| /// it is intentionally independent of the authentication mechanism. | ||
| pub const IDENTITY_USER_ID_METADATA: &str = "identity.user_id"; | ||
|
|
There was a problem hiding this comment.
Since we'll eventually remove basic_auth, I don't know if this will survive that intact or not. I recommend storing it under the basic_auth filter code for now, or at least behind the experimental build flag as I have suspicions things are going to change.
| if raw.username.trim().is_empty() { | ||
| return Err("username must not be empty".to_owned()); | ||
| } | ||
| if raw.username.len() > 256 { |
There was a problem hiding this comment.
We should promote this to a constant
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:
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:
The reusable contract is:
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
IDENTITY_USER_ID_METADATAconstant.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:
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:
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
cargo test -p praxis-proxy-filter context --locked
git diff --check
Coverage includes:
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.