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
1 change: 1 addition & 0 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions crates/auths-oidc-bridge/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ categories = ["authentication", "web-programming"]
[features]
default = []
github-oidc = ["dep:reqwest"]
oidc-policy = ["dep:auths-policy"]
oidc-trust = ["dep:auths-policy"]

[[bin]]
name = "auths-oidc-bridge"
Expand All @@ -24,6 +26,7 @@ name = "auths_oidc_bridge"
path = "src/lib.rs"

[dependencies]
auths-policy = { workspace = true, optional = true }
auths-telemetry.workspace = true
auths-verifier = { workspace = true, features = ["native"] }

Expand Down
39 changes: 39 additions & 0 deletions crates/auths-oidc-bridge/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ pub struct BridgeConfig {
/// Log level filter.
pub log_level: String,

/// Path to a JSON file containing the workload policy expression.
#[cfg(feature = "oidc-policy")]
pub workload_policy_path: Option<PathBuf>,

/// Inline JSON string containing the workload policy expression.
#[cfg(feature = "oidc-policy")]
pub workload_policy_json: Option<String>,

/// Path to a JSON file containing trust registry entries.
#[cfg(feature = "oidc-trust")]
pub trust_registry_path: Option<PathBuf>,

/// GitHub OIDC issuer URL (default: "https://token.actions.githubusercontent.com").
#[cfg(feature = "github-oidc")]
pub github_oidc_issuer: Option<String>,
Expand Down Expand Up @@ -84,6 +96,12 @@ impl Default for BridgeConfig {
admin_token: None,
enable_cors: false,
log_level: "info".to_string(),
#[cfg(feature = "oidc-policy")]
workload_policy_path: None,
#[cfg(feature = "oidc-policy")]
workload_policy_json: None,
#[cfg(feature = "oidc-trust")]
trust_registry_path: None,
#[cfg(feature = "github-oidc")]
github_oidc_issuer: None,
#[cfg(feature = "github-oidc")]
Expand Down Expand Up @@ -185,6 +203,27 @@ impl BridgeConfig {
self
}

/// Set the workload policy JSON file path.
#[cfg(feature = "oidc-policy")]
pub fn with_workload_policy_path(mut self, path: PathBuf) -> Self {
self.workload_policy_path = Some(path);
self
}

/// Set the workload policy as an inline JSON string.
#[cfg(feature = "oidc-policy")]
pub fn with_workload_policy_json(mut self, json: impl Into<String>) -> Self {
self.workload_policy_json = Some(json.into());
self
}

/// Set the trust registry JSON file path.
#[cfg(feature = "oidc-trust")]
pub fn with_trust_registry_path(mut self, path: PathBuf) -> Self {
self.trust_registry_path = Some(path);
self
}

/// Set the GitHub OIDC issuer URL.
#[cfg(feature = "github-oidc")]
pub fn with_github_oidc_issuer(mut self, issuer: impl Into<String>) -> Self {
Expand Down
47 changes: 47 additions & 0 deletions crates/auths-oidc-bridge/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,34 @@ pub enum BridgeError {
#[cfg(feature = "github-oidc")]
#[error("actor mismatch: expected {expected}, got {actual}")]
ActorMismatch { expected: String, actual: String },

/// OIDC provider not found in trust registry.
#[cfg(feature = "oidc-trust")]
#[error("OIDC provider not in trust registry: {provider}")]
ProviderNotTrusted { provider: String },

/// Repository not allowed for this provider.
#[cfg(feature = "oidc-trust")]
#[error("repository not allowed for provider {provider}: {repo}")]
RepositoryNotAllowed { repo: String, provider: String },

/// Requested capabilities not allowed by trust registry.
#[cfg(feature = "oidc-trust")]
#[error("no allowed capabilities match request")]
CapabilityNotAllowed {
requested: Vec<String>,
allowed: Vec<String>,
},

/// Workload policy denied the token exchange.
#[cfg(feature = "oidc-policy")]
#[error("policy denied: {0}")]
PolicyDenied(String),

/// Policy compilation failed at startup.
#[cfg(feature = "oidc-policy")]
#[error("policy compilation failed: {0}")]
PolicyCompilationFailed(String),
}

/// Error response body.
Expand Down Expand Up @@ -124,6 +152,25 @@ impl IntoResponse for BridgeError {
}
#[cfg(feature = "github-oidc")]
BridgeError::ActorMismatch { .. } => (StatusCode::FORBIDDEN, "ACTOR_MISMATCH"),
#[cfg(feature = "oidc-trust")]
BridgeError::ProviderNotTrusted { .. } => {
(StatusCode::FORBIDDEN, "PROVIDER_NOT_TRUSTED")
}
#[cfg(feature = "oidc-trust")]
BridgeError::RepositoryNotAllowed { .. } => {
(StatusCode::FORBIDDEN, "REPOSITORY_NOT_ALLOWED")
}
#[cfg(feature = "oidc-trust")]
BridgeError::CapabilityNotAllowed { .. } => {
(StatusCode::FORBIDDEN, "CAPABILITY_NOT_ALLOWED")
}
#[cfg(feature = "oidc-policy")]
BridgeError::PolicyDenied(_) => (StatusCode::FORBIDDEN, "POLICY_DENIED"),
#[cfg(feature = "oidc-policy")]
BridgeError::PolicyCompilationFailed(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"POLICY_COMPILATION_FAILED",
),
};

let retry_after = if let BridgeError::RateLimited {
Expand Down
80 changes: 77 additions & 3 deletions crates/auths-oidc-bridge/src/issuer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,18 @@ impl OidcIssuer {
///
/// Args:
/// * `request`: The exchange request containing the attestation chain.
/// * `workload_policy`: Optional compiled policy to evaluate before issuance (oidc-policy feature).
/// * `github_cross_ref`: Optional GitHub OIDC cross-reference result (github-oidc feature).
///
/// Usage:
/// ```ignore
/// let response = issuer.exchange(&request, None)?;
/// let response = issuer.exchange(&request, None, None)?;
/// ```
pub async fn exchange(
&self,
request: &ExchangeRequest,
#[cfg(feature = "oidc-trust")] trust_registry: Option<&auths_policy::TrustRegistry>,
#[cfg(feature = "oidc-policy")] workload_policy: Option<&auths_policy::CompiledPolicy>,
#[cfg(feature = "github-oidc")] github_cross_ref: Option<
&crate::cross_reference::CrossReferenceResult,
>,
Expand Down Expand Up @@ -108,7 +112,7 @@ impl OidcIssuer {
.map(|a| a.issuer.to_string())
.unwrap_or_default();

// 7. Extract capabilities from last attestation, applying scope-down
// 7. Extract capabilities from last attestation
let chain_granted: Vec<String> = chain
.last()
.map(|a| {
Expand All @@ -118,6 +122,61 @@ impl OidcIssuer {
.collect()
})
.unwrap_or_default();

// 7.5. Trust registry: narrow capabilities and cap TTL
#[cfg(feature = "oidc-trust")]
let (chain_granted, ttl_secs) = if let Some(registry) = trust_registry {
let provider = request.provider_issuer.as_deref().ok_or_else(|| {
BridgeError::InvalidRequest(
"provider_issuer is required when trust registry is configured".into(),
)
})?;

let entry =
registry
.lookup(provider)
.ok_or_else(|| BridgeError::ProviderNotTrusted {
provider: provider.to_string(),
})?;

if let Some(ref repo) = request.repository
&& !entry.repo_allowed(repo)
{
return Err(BridgeError::RepositoryNotAllowed {
repo: repo.clone(),
provider: provider.to_string(),
});
}

let narrowed: Vec<String> = chain_granted
.iter()
.filter(|c| {
entry
.allowed_capabilities
.iter()
.any(|a| a.as_str() == c.as_str())
})
.cloned()
.collect();

if narrowed.is_empty() && !chain_granted.is_empty() {
return Err(BridgeError::CapabilityNotAllowed {
requested: chain_granted,
allowed: entry
.allowed_capabilities
.iter()
.map(|c| c.as_str().to_string())
.collect(),
});
}

let capped_ttl = std::cmp::min(ttl_secs, entry.max_token_ttl_seconds);
(narrowed, capped_ttl)
} else {
(chain_granted, ttl_secs)
};

// 7.6. Apply scope-down
let capabilities =
Self::scope_capabilities(&chain_granted, request.requested_capabilities.as_deref())?;

Expand Down Expand Up @@ -159,7 +218,22 @@ impl OidcIssuer {
github_repository: None,
};

// 9. Sign JWT with RS256
// 9. Evaluate workload policy (if configured)
#[cfg(feature = "oidc-policy")]
if let Some(policy) = workload_policy {
// INVARIANT: u64 epoch seconds always fits in DateTime (overflows at ~292 billion years)
#[allow(clippy::expect_used)]
let now_dt = chrono::DateTime::from_timestamp(now as i64, 0)
.expect("u64 epoch seconds always fits in DateTime");
let ctx = crate::policy_adapter::build_eval_context_from_oidc(&claims, now_dt)?;
let decision = auths_policy::evaluate_strict(policy, &ctx);
if decision.outcome != auths_policy::Outcome::Allow {
tracing::info!(reason = %decision.message, "auths.exchange.policy_denied");
return Err(BridgeError::PolicyDenied(decision.message));
}
}

// 10. Sign JWT with RS256
let mut header = Header::new(Algorithm::RS256);
header.kid = Some(self.kid.clone());

Expand Down
2 changes: 2 additions & 0 deletions crates/auths-oidc-bridge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ pub mod error;
pub mod github_oidc;
pub mod issuer;
pub mod jwks;
#[cfg(feature = "oidc-policy")]
pub mod policy_adapter;
pub mod rate_limit;
pub mod routes;
pub mod state;
Expand Down
Loading