Skip to main content

rskit_auth/oidc/
http.rs

1use std::fmt;
2
3use async_trait::async_trait;
4use rskit_httpclient::{Auth, HttpClient, HttpClientConfig, Request};
5use serde_json::Value;
6
7use super::error::OidcError;
8
9#[async_trait]
10/// Minimal async HTTP client contract used by OIDC.
11pub trait OidcHttpClient: Send + Sync {
12    /// Fetch JSON from a URL, optionally using bearer authentication.
13    async fn get_json(&self, url: &str, bearer_token: Option<&str>) -> Result<Value, OidcError>;
14}
15
16/// Default OIDC HTTP client backed by the canonical `rskit-httpclient`.
17pub struct ReqwestOidcHttpClient {
18    client: HttpClient,
19}
20
21impl ReqwestOidcHttpClient {
22    /// Create a default OIDC HTTP client using the canonical rskit HTTP client.
23    ///
24    /// # Errors
25    /// Returns an error when the HTTP client cannot be constructed.
26    pub fn new() -> Result<Self, OidcError> {
27        Self::with_config(HttpClientConfig::new())
28    }
29
30    /// Create an OIDC HTTP client with explicit HTTP configuration.
31    ///
32    /// # Errors
33    /// Returns an error when the HTTP client cannot be constructed.
34    pub fn with_config(config: HttpClientConfig) -> Result<Self, OidcError> {
35        let client = HttpClient::new(sanitize_oidc_config(config))
36            .map_err(|error| OidcError::ProviderUnreachable(error.to_string()))?;
37        Ok(Self { client })
38    }
39}
40
41impl fmt::Debug for ReqwestOidcHttpClient {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        f.debug_struct("ReqwestOidcHttpClient")
44            .field("client", &"<redacted>")
45            .finish()
46    }
47}
48
49fn sanitize_oidc_config(mut config: HttpClientConfig) -> HttpClientConfig {
50    config.base_url = None;
51    config.default_headers.clear();
52    config.auth = None;
53    config
54}
55
56#[async_trait]
57impl OidcHttpClient for ReqwestOidcHttpClient {
58    async fn get_json(&self, url: &str, bearer_token: Option<&str>) -> Result<Value, OidcError> {
59        let mut request = Request::get(url);
60        if let Some(token) = bearer_token {
61            request = request.auth(Auth::bearer(token));
62        }
63        let response = self
64            .client
65            .send(request)
66            .await
67            .map_err(|error| OidcError::ProviderUnreachable(error.to_string()))?;
68        response
69            .checked_json()
70            .map_err(|error| OidcError::ProviderUnreachable(error.to_string()))
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use std::time::Duration;
77
78    use http::header::AUTHORIZATION;
79    use rskit_httpclient::{Auth, HttpClientConfig};
80    use rskit_security::BEARER_AUTH_SCHEME;
81
82    use super::ReqwestOidcHttpClient;
83
84    #[test]
85    fn oidc_http_client_constructs_with_default_config() {
86        assert!(ReqwestOidcHttpClient::new().is_ok());
87    }
88
89    #[test]
90    fn oidc_http_client_accepts_explicit_config() {
91        let config = HttpClientConfig::new().with_timeout(Duration::from_secs(5));
92        assert!(ReqwestOidcHttpClient::with_config(config).is_ok());
93    }
94
95    #[test]
96    fn oidc_http_client_redacts_debug_output() {
97        let config = HttpClientConfig::new().with_auth(Auth::bearer("secret-token"));
98        let client = ReqwestOidcHttpClient::with_config(config).unwrap();
99
100        let formatted = format!("{client:?}");
101
102        assert!(formatted.contains("<redacted>"));
103        assert!(!formatted.contains("secret-token"));
104        assert!(!formatted.contains("HttpClientConfig"));
105    }
106
107    #[test]
108    fn oidc_http_client_drops_cross_origin_defaults() {
109        let config = HttpClientConfig::new()
110            .with_base_url("https://internal.example")
111            .with_header(
112                AUTHORIZATION.as_str(),
113                format!("{BEARER_AUTH_SCHEME} leaked"),
114            )
115            .with_header("x-api-key", "leaked")
116            .with_auth(Auth::api_key("x-other-key", "leaked"));
117
118        let client = ReqwestOidcHttpClient::with_config(config).unwrap();
119        let config = client.client.config();
120
121        assert!(config.base_url.is_none());
122        assert!(config.default_headers.is_empty());
123        assert!(config.auth.is_none());
124    }
125}