xet_client/cas_client/
auth.rs1use std::fmt::Debug;
2use std::sync::Arc;
3#[cfg(not(target_family = "wasm"))]
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use reqwest_middleware::ClientWithMiddleware;
7use thiserror::Error;
8use tracing::info;
9use xet_runtime::core::XetContext;
10
11use crate::common::auth::CredentialHelper;
12
13#[derive(Debug, Error)]
14#[non_exhaustive]
15pub enum AuthError {
16 #[error("Refresh function: {0} is not callable")]
17 RefreshFunctionNotCallable(String),
18
19 #[error("Token refresh failed: {0}")]
20 TokenRefreshFailure(String),
21}
22
23impl AuthError {
24 pub fn token_refresh_failure(err: impl ToString) -> Self {
25 Self::TokenRefreshFailure(err.to_string())
26 }
27}
28
29const REFRESH_BUFFER_SEC: u64 = 30;
31
32pub type TokenInfo = (String, u64);
35
36#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
38#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
39pub trait TokenRefresher: Send + Sync {
40 async fn refresh(&self) -> Result<TokenInfo, AuthError>;
42}
43
44#[derive(Debug)]
45pub struct NoOpTokenRefresher;
46
47#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
48#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
49impl TokenRefresher for NoOpTokenRefresher {
50 async fn refresh(&self) -> Result<TokenInfo, AuthError> {
51 Ok(("token".to_string(), 0))
52 }
53}
54
55#[derive(Debug)]
56pub struct ErrTokenRefresher;
57
58#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
59#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
60impl TokenRefresher for ErrTokenRefresher {
61 async fn refresh(&self) -> Result<TokenInfo, AuthError> {
62 Err(AuthError::RefreshFunctionNotCallable("Token refresh not expected".to_string()))
63 }
64}
65
66pub struct DirectRefreshRouteTokenRefresher {
71 ctx: XetContext,
72 refresh_route: String,
73 client: ClientWithMiddleware,
74 cred_helper: Option<Arc<dyn CredentialHelper>>,
75}
76
77impl std::fmt::Debug for DirectRefreshRouteTokenRefresher {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 f.debug_struct("DirectRefreshRouteTokenRefresher")
80 .field("refresh_route", &self.refresh_route)
81 .finish_non_exhaustive()
82 }
83}
84
85impl DirectRefreshRouteTokenRefresher {
86 pub fn new(
87 ctx: XetContext,
88 refresh_route: impl Into<String>,
89 client: ClientWithMiddleware,
90 cred_helper: Option<Arc<dyn CredentialHelper>>,
91 ) -> Self {
92 Self {
93 ctx,
94 refresh_route: refresh_route.into(),
95 client,
96 cred_helper,
97 }
98 }
99
100 pub async fn get_cas_jwt(&self) -> Result<crate::hub_client::CasJWTInfo, crate::ClientError> {
101 let client = self.client.clone();
102 let refresh_route = self.refresh_route.clone();
103 let cred_helper = self.cred_helper.clone();
104
105 let jwt_info: crate::hub_client::CasJWTInfo =
106 super::retry_wrapper::RetryWrapper::new(self.ctx.clone(), "xet-token")
107 .run_and_extract_json(move || {
108 let refresh_route = refresh_route.clone();
109 let client = client.clone();
110 let cred_helper = cred_helper.clone();
111 async move {
112 let req = client
113 .get(&refresh_route)
114 .with_extension(crate::common::http_client::Api("xet-token"));
115 let req = if let Some(helper) = cred_helper {
116 helper
117 .fill_credential(req)
118 .await
119 .map_err(reqwest_middleware::Error::middleware)?
120 } else {
121 req
122 };
123 req.send().await
124 }
125 })
126 .await?;
127
128 Ok(jwt_info)
129 }
130}
131
132#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
133#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
134impl TokenRefresher for DirectRefreshRouteTokenRefresher {
135 async fn refresh(&self) -> Result<TokenInfo, AuthError> {
136 let jwt_info = self.get_cas_jwt().await.map_err(AuthError::token_refresh_failure)?;
137
138 Ok((jwt_info.access_token, jwt_info.exp))
139 }
140}
141
142#[derive(Clone)]
144pub struct AuthConfig {
145 pub token: String,
147 pub token_expiration: u64,
149 pub token_refresher: Arc<dyn TokenRefresher>,
151}
152
153impl Debug for AuthConfig {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 f.debug_struct("AuthConfig")
156 .field("token", &self.token)
157 .field("token_expiration", &self.token_expiration)
158 .finish_non_exhaustive()
159 }
160}
161
162impl AuthConfig {
163 pub fn maybe_new(
165 token: Option<String>,
166 token_expiry: Option<u64>,
167 token_refresher: Option<Arc<dyn TokenRefresher>>,
168 ) -> Option<Self> {
169 match (token, token_expiry, token_refresher) {
170 (token, expiry, Some(refresher)) => Some(Self {
172 token: token.unwrap_or_default(),
173 token_expiration: expiry.unwrap_or_default(),
174 token_refresher: refresher,
175 }),
176 (Some(token), expiry, None) => Some(Self {
179 token,
180 token_expiration: expiry.unwrap_or(u64::MAX),
181 token_refresher: Arc::new(ErrTokenRefresher),
182 }),
183 (_, _, _) => None,
184 }
185 }
186}
187
188pub struct TokenProvider {
189 token: String,
190 expiration: u64,
191 refresher: Arc<dyn TokenRefresher>,
192}
193
194impl TokenProvider {
195 pub fn new(cfg: &AuthConfig) -> Self {
196 Self {
197 token: cfg.token.clone(),
198 expiration: cfg.token_expiration,
199 refresher: cfg.token_refresher.clone(),
200 }
201 }
202
203 pub async fn get_valid_token(&mut self) -> Result<String, AuthError> {
204 if self.is_expired() {
205 let (new_token, new_expiry) = self.refresher.refresh().await?;
206 self.token = new_token;
207 self.expiration = new_expiry;
208 info!(new_expiry = new_expiry, "Token refreshed");
209 }
210 Ok(self.token.clone())
211 }
212
213 fn is_expired(&self) -> bool {
214 #[cfg(not(target_family = "wasm"))]
215 let cur_time = SystemTime::now()
216 .duration_since(UNIX_EPOCH)
217 .map(|d| d.as_secs())
218 .unwrap_or(u64::MAX);
219 #[cfg(target_family = "wasm")]
220 let cur_time = web_time::SystemTime::now()
221 .duration_since(web_time::UNIX_EPOCH)
222 .map(|d| d.as_secs())
223 .unwrap_or(u64::MAX);
224 self.expiration <= cur_time + REFRESH_BUFFER_SEC
225 }
226}