jwk_simple/jwks/
remote.rs

1//! Remote JWKS fetching without caching.
2//!
3//! This module provides [`RemoteKeySet`], which fetches keys from an HTTP endpoint
4//! on every request. For production use, consider wrapping with [`CachedKeySet`](super::CachedKeySet).
5
6use std::time::Duration;
7
8use crate::error::Result;
9use crate::jwk::Key;
10
11use super::{KeySet, KeySource};
12
13/// Default timeout for HTTP requests (30 seconds).
14pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
15
16/// A JWKS source that fetches from an HTTP endpoint on every request.
17///
18/// This implementation does **not** cache keys. Every call to [`get_key`](KeySource::get_key)
19/// or [`get_keyset`](KeySource::get_keyset) will make an HTTP request.
20///
21/// For production use with high request volumes, wrap this in [`CachedKeySet`](super::CachedKeySet):
22///
23/// ```ignore
24/// use jwk_simple::{RemoteKeySet, CachedKeySet};
25/// use std::time::Duration;
26///
27/// let remote = RemoteKeySet::new("https://example.com/.well-known/jwks.json");
28/// let cached = CachedKeySet::new(remote, Duration::from_secs(300));
29/// ```
30///
31/// # Examples
32///
33/// ```ignore
34/// use jwk_simple::{KeySource, RemoteKeySet};
35///
36/// let jwks = RemoteKeySet::new("https://example.com/.well-known/jwks.json");
37/// let key = jwks.get_key("my-key-id").await?;
38/// ```
39///
40/// # Custom HTTP Client
41///
42/// You can provide a custom [`reqwest::Client`] for full control over HTTP behavior:
43///
44/// ```ignore
45/// use jwk_simple::RemoteKeySet;
46/// use std::time::Duration;
47///
48/// let client = reqwest::Client::builder()
49///     .timeout(Duration::from_secs(10))
50///     .user_agent("my-app/1.0")
51///     .build()
52///     .unwrap();
53///
54/// let jwks = RemoteKeySet::new_with_client(
55///     "https://example.com/.well-known/jwks.json",
56///     client,
57/// );
58/// ```
59#[derive(Debug, Clone)]
60pub struct RemoteKeySet {
61    url: String,
62    client: reqwest::Client,
63}
64
65impl RemoteKeySet {
66    /// Creates a new `RemoteKeySet` from a URL.
67    ///
68    /// Uses a default HTTP client with a 30-second timeout. To customize the client,
69    /// use [`new_with_client`](Self::new_with_client).
70    ///
71    /// # Arguments
72    ///
73    /// * `url` - The JWKS endpoint URL.
74    pub fn new(url: impl Into<String>) -> Self {
75        let client = reqwest::Client::builder()
76            .timeout(DEFAULT_TIMEOUT)
77            .build()
78            .expect("failed to build default HTTP client");
79
80        Self {
81            url: url.into(),
82            client,
83        }
84    }
85
86    /// Creates a new `RemoteKeySet` with a custom HTTP client.
87    ///
88    /// Use this to configure custom timeouts, headers, proxies, TLS settings, etc.
89    ///
90    /// # Arguments
91    ///
92    /// * `url` - The JWKS endpoint URL.
93    /// * `client` - A configured [`reqwest::Client`].
94    ///
95    /// # Examples
96    ///
97    /// ```ignore
98    /// use jwk_simple::RemoteKeySet;
99    /// use std::time::Duration;
100    ///
101    /// let client = reqwest::Client::builder()
102    ///     .timeout(Duration::from_secs(10))
103    ///     .build()
104    ///     .unwrap();
105    ///
106    /// let jwks = RemoteKeySet::new_with_client(
107    ///     "https://example.com/.well-known/jwks.json",
108    ///     client,
109    /// );
110    /// ```
111    pub fn new_with_client(url: impl Into<String>, client: reqwest::Client) -> Self {
112        Self {
113            url: url.into(),
114            client,
115        }
116    }
117
118    /// Fetches the JWKS from the remote endpoint.
119    async fn fetch(&self) -> Result<KeySet> {
120        let response = self.client.get(&self.url).send().await?;
121        let json = response.text().await?;
122
123        Ok(serde_json::from_str::<KeySet>(&json)?)
124    }
125}
126
127#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
128#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
129impl KeySource for RemoteKeySet {
130    async fn get_key(&self, kid: &str) -> Result<Option<Key>> {
131        let keyset = self.fetch().await?;
132
133        Ok(keyset.find_by_kid(kid).cloned())
134    }
135
136    async fn get_keyset(&self) -> Result<KeySet> {
137        self.fetch().await
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn test_remote_jwks_new() {
147        let _jwks = RemoteKeySet::new("https://example.com/jwks");
148    }
149
150    #[test]
151    fn test_remote_jwks_new_with_client() {
152        let client = reqwest::Client::builder()
153            .timeout(Duration::from_secs(10))
154            .build()
155            .unwrap();
156
157        let _jwks = RemoteKeySet::new_with_client("https://example.com/jwks", client);
158    }
159}