Skip to content

feat(delegation): carry the auth scheme on a minted delegated token #139

Open

Description

@terylt

Description

RawDelegatedToken carries the token bytes, the outbound header name, the audience, the scopes and the expiry, but no indication of how the token should be presented. The consuming host therefore has to guess, and Praxis guesses Bearer:

let bearer = zeroize::Zeroizing::new(format!("Bearer {}", tok.token.as_str()));

(crates/filter/src/builtins/http/security/policy/filter.rs:2172 in the praxis repo.)

default_outbound_header is configurable on the delegator (builtins/plugins/delegator-oauth/src/config.rs:52, defaulting to Authorization), so an operator can already target a non standard header. What they cannot do is send anything other than Bearer <token> into it. A downstream that wants a bare token in X-Api-Key, or a non Bearer scheme, cannot be served, and the failure is quiet: the header arrives with a Bearer prefix the upstream does not expect, and the call fails as an auth error at the backend rather than as a config error at the gateway.

This is most likely to bite with X-Api-Key style headers, which is the natural pairing for the API key identity work in #125.

Shape

Add an optional scheme to RawDelegatedToken, defaulting to Bearer so existing behaviour is unchanged:

/// How the token is presented in `outbound_header`. `None` sends the
/// token bytes alone; `Some("Bearer")` sends `Bearer <token>`.
pub scheme: Option<String>,

RawDelegatedToken::new currently takes five positional arguments and adding a sixth is unpleasant. The type is already constructed in a couple of places, so this is a good moment to either mark it #[non_exhaustive] with a builder, matching what DelegationKey does, or add a with_scheme setter alongside the existing constructor.

The field must serialize. Unlike token it is not secret, and a host reading a serialized extension needs it to render the header correctly.

Praxis follow-on

Once the field exists, attach_delegated_tokens honours it instead of hardcoding the prefix. A None scheme writes the token bytes alone. This is a small change but it cannot land first, so the two need sequencing.

Related, not in scope

Outbound header collisions are resolved first-writer-wins by lowest audience alphabetically, with a warning (filter.rs:2140 onward in praxis). That is deterministic and logged, so it is not a bug, but two delegators both defaulting to Authorization is a plausible config mistake and the surviving token is chosen by something an operator would not predict. Worth a separate look if it shows up in practice.

Acceptance criteria

  • RawDelegatedToken carries an optional scheme, defaulting to Bearer semantics for existing callers.
  • The field serializes, unlike token.
  • delegator-oauth exposes the scheme in its config alongside default_outbound_header.
  • A follow-on issue is filed against praxis for honouring the field in attach_delegated_tokens.
  • A test covers a None scheme producing a header value with no prefix.
  • The "Scheme prefixes" open question on feat(delegator-vault): mint downstream credentials from Vault, scoped to the caller #91 is closed, with that issue's X-API-Key acceptance criterion now satisfiable.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions