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]
10pub trait OidcHttpClient: Send + Sync {
12 async fn get_json(&self, url: &str, bearer_token: Option<&str>) -> Result<Value, OidcError>;
14}
15
16pub struct ReqwestOidcHttpClient {
18 client: HttpClient,
19}
20
21impl ReqwestOidcHttpClient {
22 pub fn new() -> Result<Self, OidcError> {
27 Self::with_config(HttpClientConfig::new())
28 }
29
30 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}