Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,23 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
> - **Fixed**: for any bug fixes.
> - **Security**: in case of vulnerabilities.

## [0.2.3] - unreleased

### Added

- **Multi-principal delegation.** A `delegate(...)` step can now name **whose** identity the minted token speaks for (`subject: user | client | caller_workload | this_workload`) and **who** is acting (`actor: user | client | caller_workload`, an RFC 8693 `actor_token` recording `act` alongside `sub`). The mode is *derived* from the subject, never declared, so a route can't claim on-behalf-of-user while handing over a workload SVID. Adds SPIFFE JWT-SVID workload ingress (`role: caller_workload`, validated into `caller_workload.*` and stashed as `TokenKind::SpiffeJwt`) and, for `subject: caller_workload`, a two-leg OAuth delegator (SVID as an RFC 7523 `client_assertion` → base token → RFC 8693 exchange). (#131)
- **Top-level `groups:` config section.** Reusable policy bundles (authentication + authorization + plugins) now live at a canonical top-level `groups:`, and a route joins one with a first-class `groups:` field (string-or-list). `groups:` is sugar over tags — it folds into the route's tag set at resolution, so host-injected runtime tags still join groups the same way. A route naming an undefined group is rejected at load. (#131)

### Changed

- **BREAKING: `TokenRole::Workload` renamed to `TokenRole::CallerWorkload`.** A serde `alias = "workload"` keeps existing serialized config loading, but the Rust symbol is renamed — downstream Rust code must update. (#131)
- **BREAKING: `DelegationMode::AsGateway` renamed to `AsThisWorkload`.** A serde `alias = "as_gateway"` keeps persisted values deserializing. (#131)
- **BREAKING: `DelegationKey` is now `#[non_exhaustive]`** and gained a `client_id` field (partitioning the delegated-token cache per calling OAuth client, mirroring `workload_id`). Construct it via `DelegationKey::new(mode, audience, scopes)` + the `with_subject_id` / `with_workload_id` / `with_client_id` setters rather than a struct literal. (#131)

### Deprecated

- The reserved `all` group and the `global.policies:` bundle location, in favor of the top-level `groups:` section. Both still load. (#131)

## [0.2.2] - 2026-07-15

### Added
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions builtins/plugins/delegator-oauth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
chrono = { workspace = true }
tracing = { workspace = true }

# `base64` decodes the minted token's JWT payload for a best-effort,
# read-only interop check (did the IdP honor the RFC 8693 `actor_token`
# and emit an `act` claim). We never verify the signature here — the
# token is already trusted, having just come from our own IdP roundtrip.
base64 = "0.22"

# Secret-clearing wrapper for client credentials in memory.
zeroize = { version = "1.8", features = ["zeroize_derive"] }
Expand Down
40 changes: 40 additions & 0 deletions builtins/plugins/delegator-oauth/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,28 @@ pub struct OAuthDelegatorConfig {
/// deployments must leave this at the default (`false`).
#[serde(default)]
pub insecure_http: bool,

/// The `actor_token_type` we tell the IdP the RFC 8693
/// `actor_token` is — a token-type URN. Defaults to
/// `...:token-type:jwt` because the actor is almost always a
/// JWT-SVID. Only consulted when the `DelegationPayload` carries a
/// non-empty `actor_token` (attached upstream by the invoker from
/// the inbound workload SVID); otherwise the exchange stays
/// single-token and behaves exactly as before.
#[serde(default = "default_actor_token_type")]
pub actor_token_type: String,

/// The `client_assertion_type` used in leg 1 of a workload
/// delegation (`subject: caller_workload`), where the calling
/// agent authenticates by presenting its JWT-SVID as an RFC 7523
/// client assertion rather than a secret. Defaults to the
/// SPIFFE-specific URN from draft-ietf-oauth-spiffe-client-auth —
/// NOT the generic `...:jwt-bearer` — because that's what a SPIFFE
/// authorization server (e.g. Keycloak's SPIFFE provider) expects.
/// Only consulted on the `caller_workload` path; every other
/// subject authenticates with the client secret as before.
#[serde(default = "default_workload_assertion_type")]
pub workload_assertion_type: String,
}

/// Where the gateway's OAuth client secret is loaded from. Three
Expand All @@ -88,6 +110,14 @@ fn default_subject_token_type() -> String {
"urn:ietf:params:oauth:token-type:access_token".to_string()
}

fn default_actor_token_type() -> String {
"urn:ietf:params:oauth:token-type:jwt".to_string()
}

fn default_workload_assertion_type() -> String {
"urn:ietf:params:oauth:client-assertion-type:jwt-spiffe".to_string()
}

fn default_timeout_seconds() -> u64 {
5
}
Expand Down Expand Up @@ -137,6 +167,16 @@ mod tests {
assert_eq!(cfg.client_id, "gateway");
assert_eq!(cfg.timeout_seconds, 5);
assert_eq!(cfg.default_outbound_header, "Authorization");
// actor_token_type defaults to the JWT token-type URN (the
// actor is almost always a JWT-SVID); only used when the
// payload carries a non-empty actor_token.
assert_eq!(cfg.actor_token_type, "urn:ietf:params:oauth:token-type:jwt");
// workload_assertion_type defaults to the SPIFFE-specific
// client-assertion URN (leg 1 of a caller_workload delegation).
assert_eq!(
cfg.workload_assertion_type,
"urn:ietf:params:oauth:client-assertion-type:jwt-spiffe"
);
}

#[test]
Expand Down
Loading
Loading