mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Add GitHub Actions OIDC token acquisition to the responses API proxy (#55297)
## What changed Add a `GithubOidc` implementation of `SubjectTokenSource` that requests a fresh token using a bearer credential read from stdin. Validate HTTPS request URLs and replace the audience query parameter while preserving other parameters. CLI integration is not included. Add a token HTTP client that disables redirects and request logging, applies timeouts, and bounds response bodies. Keep token response buffers in zeroizing storage and exclude credentials, request URLs, and response bodies from errors. Generalize the locked stdin credential reader for bounded OIDC bearer tokens while preserving API key validation. ## Testing Add tests for fresh token acquisition, short reads, credential size limits, injection rejection, redirect refusal, and credential-safe errors and logs. Extend API key tests to retain the stricter character restrictions. GitOrigin-RevId: fd7767178f475b8161734f29d29108c795839185
This commit is contained in:
@@ -1,9 +1,76 @@
|
||||
//! Supplies sensitive request headers without copying the locked API key.
|
||||
//! Token clients never follow redirects or emit credential-bearing URL diagnostics.
|
||||
|
||||
use std::fmt;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::Result;
|
||||
use reqwest::header::HeaderValue;
|
||||
use anyhow::anyhow;
|
||||
use codex_http_client::ClientRouteClass;
|
||||
use codex_http_client::HttpClient;
|
||||
use codex_http_client::HttpClientBuilder;
|
||||
use codex_http_client::HttpClientFactory;
|
||||
use codex_http_client::HttpResponse;
|
||||
use codex_http_client::OutboundProxyPolicy;
|
||||
use codex_http_client::RouteAwareClientPool;
|
||||
use http::HeaderValue;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::time::Duration;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
#[cfg_attr(not(test), allow(dead_code, reason = "used by a later stack layer"))]
|
||||
pub(crate) const MAX_TOKEN_BYTES: usize = 16 * 1024;
|
||||
#[cfg_attr(not(test), allow(dead_code, reason = "used by a later stack layer"))]
|
||||
const MAX_RESPONSE_BYTES: usize = 64 * 1024;
|
||||
#[cfg_attr(not(test), allow(dead_code, reason = "used by a later stack layer"))]
|
||||
pub(crate) const TOKEN_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
/// Supplies a fresh JWT whenever an OpenAI credential needs to be issued.
|
||||
/// Acquisition stays asynchronous so callers can compose it with the exchange.
|
||||
/// Implementations must not include tokens, credential-bearing URLs, or upstream
|
||||
/// response bodies in errors. The exchange deliberately has no issuer-specific config.
|
||||
#[cfg_attr(not(test), allow(dead_code, reason = "used by a later stack layer"))]
|
||||
pub(crate) trait SubjectTokenSource: Send {
|
||||
fn subject_token<'a>(&'a mut self, client: &'a TokenClient) -> SubjectTokenFuture<'a>;
|
||||
}
|
||||
|
||||
// Keep the source usable as a trait object while acquisition runs asynchronously.
|
||||
pub(crate) type SubjectTokenFuture<'a> =
|
||||
Pin<Box<dyn Future<Output = Result<Zeroizing<String>>> + Send + 'a>>;
|
||||
|
||||
/// Credential requests must not follow redirects or log URLs or response headers.
|
||||
/// The private field prevents other modules from wrapping arbitrary HTTP clients.
|
||||
#[cfg_attr(not(test), allow(dead_code, reason = "used by a later stack layer"))]
|
||||
pub(crate) struct TokenClient(HttpClient);
|
||||
|
||||
#[cfg_attr(not(test), allow(dead_code, reason = "used by a later stack layer"))]
|
||||
impl TokenClient {
|
||||
#[allow(
|
||||
dead_code,
|
||||
reason = "federation creates the routed client in the next stack layer"
|
||||
)]
|
||||
pub(crate) fn new() -> Self {
|
||||
// The standalone proxy does not load Codex's session config. For its
|
||||
// opt-in auth flow, resolve system and environment proxy settings per URL.
|
||||
let factory = HttpClientFactory::new(OutboundProxyPolicy::RespectSystemProxy);
|
||||
Self(
|
||||
RouteAwareClientPool::with_builder(factory, ClientRouteClass::Auth, Self::builder())
|
||||
.into_client(),
|
||||
)
|
||||
}
|
||||
|
||||
fn builder() -> HttpClientBuilder {
|
||||
HttpClientBuilder::new()
|
||||
.connect_timeout(Duration::from_secs(10))
|
||||
.without_redirects()
|
||||
.without_request_logging()
|
||||
}
|
||||
|
||||
pub(crate) fn http(&self) -> &HttpClient {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum Auth {
|
||||
ApiKey(&'static str),
|
||||
@@ -44,6 +111,35 @@ impl Auth {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(test), allow(dead_code, reason = "used by a later stack layer"))]
|
||||
pub(crate) async fn read_response<T: DeserializeOwned>(
|
||||
mut response: HttpResponse,
|
||||
source: &str,
|
||||
) -> Result<T> {
|
||||
if !response.status().is_success() {
|
||||
// Never read or log the error body; it may echo a credential. Redirects are
|
||||
// failures too. HTTP errors can contain the URL, so don't attach them.
|
||||
return Err(anyhow!(
|
||||
"{source} token request rejected (HTTP {})",
|
||||
response.status().as_u16()
|
||||
));
|
||||
}
|
||||
// Keep secret-bearing response bytes in one allocation, including on errors.
|
||||
let mut body = Zeroizing::new(Vec::with_capacity(MAX_RESPONSE_BYTES));
|
||||
while let Some(chunk) = response
|
||||
.chunk()
|
||||
.await
|
||||
.map_err(|_| anyhow!("{source} returned an unreadable token response"))?
|
||||
{
|
||||
if chunk.len() > MAX_RESPONSE_BYTES - body.len() {
|
||||
return Err(anyhow!("{source} returned an oversized token response"));
|
||||
}
|
||||
body.extend_from_slice(&chunk);
|
||||
}
|
||||
serde_json::from_slice(&body)
|
||||
.map_err(|_| anyhow!("{source} returned an invalid token response"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "auth_tests.rs"]
|
||||
mod tests;
|
||||
pub(crate) mod tests;
|
||||
|
||||
Reference in New Issue
Block a user