rig_core/providers/internal/
auth.rs1use crate::http_client::{self, HttpClientExt};
12use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
13use std::sync::Arc;
14
15#[derive(Debug, Clone)]
17pub struct DeviceCodePrompt {
18 pub verification_uri: String,
20 pub user_code: String,
22}
23
24#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
27pub(crate) type DeviceCodeCallback = dyn Fn(DeviceCodePrompt) + Send + Sync;
28#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
29pub(crate) type DeviceCodeCallback = dyn Fn(DeviceCodePrompt);
30
31#[derive(Clone, Default)]
33pub struct DeviceCodeHandler(pub(crate) Option<Arc<DeviceCodeCallback>>);
34
35impl DeviceCodeHandler {
36 pub fn new<F>(handler: F) -> Self
38 where
39 F: Fn(DeviceCodePrompt) + WasmCompatSend + WasmCompatSync + 'static,
40 {
41 Self(Some(Arc::new(handler)))
42 }
43}
44
45impl DeviceCodeHandler {
46 #[cfg(not(target_family = "wasm"))]
49 pub(crate) fn show(&self, prompt: DeviceCodePrompt, fallback_message: &str) {
50 match &self.0 {
51 Some(callback) => callback(prompt),
52 None => println!("{fallback_message}"),
53 }
54 }
55}
56
57impl std::fmt::Debug for DeviceCodeHandler {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 if self.0.is_some() {
60 f.write_str("DeviceCodeHandler(<callback>)")
61 } else {
62 f.write_str("DeviceCodeHandler(None)")
63 }
64 }
65}
66
67#[derive(Debug, thiserror::Error)]
68pub enum AuthError {
69 #[error("{0}")]
70 Message(String),
71 #[error(transparent)]
72 Io(#[from] std::io::Error),
73 #[error(transparent)]
74 Json(#[from] serde_json::Error),
75 #[error(transparent)]
79 Http(#[from] http_client::Error),
80 #[error("token store failed: {0}")]
82 Store(#[source] crate::error::BoxError),
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct AuthContext {
88 pub api_key: crate::wire::Secret,
91 pub account_id: Option<String>,
94 pub api_base: Option<String>,
97}
98
99#[derive(Clone)]
104#[cfg_attr(target_family = "wasm", allow(dead_code))]
105pub(crate) struct Shell<S> {
106 pub(crate) source: S,
107 pub(crate) store: Arc<dyn TokenStore>,
108 pub(crate) device_code_handler: DeviceCodeHandler,
109 pub(crate) allow_device_flow: bool,
110 pub(crate) refresh_lock: Arc<futures::lock::Mutex<()>>,
112 pub(crate) remembered: Remembered,
113}
114
115impl<S> Shell<S> {
116 pub(crate) fn new(
117 source: S,
118 store: impl TokenStore + 'static,
119 device_code_handler: DeviceCodeHandler,
120 allow_device_flow: bool,
121 ) -> Self {
122 Self {
123 source,
124 store: Arc::new(store),
125 device_code_handler,
126 allow_device_flow,
127 refresh_lock: Arc::default(),
128 remembered: Remembered::default(),
129 }
130 }
131}
132
133impl<S> Shell<S> {
134 #[cfg_attr(target_family = "wasm", allow(dead_code))]
139 pub(crate) async fn hold(&self, key: &str) -> Result<Held<'_>, AuthError> {
140 let refresh = self.refresh_lock.lock().await;
141 let store = self.store.lock(key).await.map_err(AuthError::Store)?;
142 Ok(Held {
143 _refresh: refresh,
144 _store: store,
145 })
146 }
147}
148
149#[cfg_attr(target_family = "wasm", allow(dead_code))]
151pub(crate) struct Held<'a> {
152 _store: StoreLock,
153 _refresh: futures::lock::MutexGuard<'a, ()>,
154}
155
156impl<S: std::fmt::Debug> std::fmt::Debug for Shell<S> {
157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 f.debug_struct("Authenticator")
159 .field("source", &self.source)
160 .field("device_code_handler", &self.device_code_handler)
161 .field("allow_device_flow", &self.allow_device_flow)
162 .finish_non_exhaustive()
163 }
164}
165
166#[derive(Clone, Default)]
169pub(crate) struct Remembered(Arc<std::sync::Mutex<Option<(AuthContext, i64)>>>);
170
171impl Remembered {
172 pub(crate) fn live(&self, skew_seconds: i64) -> Option<AuthContext> {
174 let held = self.0.lock().ok()?;
175 let (context, expires_at) = held.as_ref()?;
176 (!token_expired(Some(*expires_at), skew_seconds)).then(|| context.clone())
177 }
178
179 pub(crate) fn keep(&self, context: &AuthContext, expires_at: Option<i64>) {
182 if let Ok(mut held) = self.0.lock() {
183 *held = expires_at.map(|expires_at| (context.clone(), expires_at));
184 }
185 }
186
187 pub(crate) fn forget(&self) {
189 if let Ok(mut held) = self.0.lock() {
190 *held = None;
191 }
192 }
193}
194
195pub(crate) fn request(method: http::Method, url: &str) -> http::request::Builder {
197 http::Request::builder().method(method).uri(url)
198}
199
200pub(crate) async fn send_json<H, T>(
204 http: &H,
205 req: http::Result<http::Request<bytes::Bytes>>,
206) -> Result<T, AuthError>
207where
208 H: HttpClientExt,
209 T: serde::de::DeserializeOwned,
210{
211 let bytes = send_bytes(http, req).await?;
212 Ok(serde_json::from_slice(&bytes)?)
213}
214
215pub(crate) async fn send_bytes<H>(
217 http: &H,
218 req: http::Result<http::Request<bytes::Bytes>>,
219) -> Result<bytes::Bytes, AuthError>
220where
221 H: HttpClientExt,
222{
223 let req = req.map_err(http_client::Error::Protocol)?;
224 let response = http.send::<_, bytes::Bytes>(req).await?;
225 Ok(response.into_body().await?)
226}
227
228pub(crate) fn token_expired(expires_at: Option<i64>, skew_seconds: i64) -> bool {
231 let now = web_time::SystemTime::now()
232 .duration_since(web_time::UNIX_EPOCH)
233 .map(|duration| duration.as_secs() as i64)
234 .unwrap_or_default();
235
236 expires_at.is_none_or(|exp| now >= exp - skew_seconds)
237}
238
239mod store;
240
241#[cfg(not(target_family = "wasm"))]
242pub use store::FileTokenStore;
243pub use store::{MemoryTokenStore, StoreLock, TokenStore};
244pub(crate) use store::{load_record, save_record};