1use crate::dto::utils::MaybeStringU64;
2use async_trait::async_trait;
3use futures_locks::RwLock;
4use reqwest::{
5 header::{HeaderMap, HeaderValue},
6 StatusCode,
7};
8use reqwest_middleware::ClientWithMiddleware;
9use serde::{Deserialize, Serialize};
10use std::time::{Duration, Instant};
11use std::{fmt::Display, sync::Arc};
12use thiserror::Error;
13
14type CustomAuthCallback =
16 dyn Fn(&mut HeaderMap, &ClientWithMiddleware) -> Result<(), AuthenticatorError> + Send + Sync;
17
18#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
19#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
20pub trait CustomAuthenticator {
23 async fn set_headers(
32 &self,
33 headers: &mut HeaderMap,
34 client: &ClientWithMiddleware,
35 ) -> Result<(), AuthenticatorError>;
36}
37
38#[derive(Clone)]
40pub enum AuthHeaderManager {
41 OIDCToken(Arc<Authenticator>),
43 FixedToken(String),
45 AuthTicket(String),
47 Custom(Arc<CustomAuthCallback>),
49 CustomAsync(Arc<dyn CustomAuthenticator + Send + Sync>),
51}
52
53impl AuthHeaderManager {
54 pub async fn set_headers(
62 &self,
63 headers: &mut HeaderMap,
64 client: &ClientWithMiddleware,
65 ) -> Result<(), AuthenticatorError> {
66 match self {
67 AuthHeaderManager::OIDCToken(a) => {
68 let token = a.get_token(client).await?;
69 let auth_header_value =
70 HeaderValue::from_str(&format!("Bearer {token}")).map_err(|e| {
71 AuthenticatorError::internal_error(
72 "Failed to set authorization bearer token".to_string(),
73 Some(e.to_string()),
74 )
75 })?;
76 headers.insert("Authorization", auth_header_value);
77 }
78 AuthHeaderManager::FixedToken(token) => {
79 let auth_header_value =
80 HeaderValue::from_str(&format!("Bearer {token}")).map_err(|e| {
81 AuthenticatorError::internal_error(
82 "Failed to set authorization bearer token".to_string(),
83 Some(e.to_string()),
84 )
85 })?;
86 headers.insert("Authorization", auth_header_value);
87 }
88 AuthHeaderManager::AuthTicket(t) => {
89 let auth_ticket_header_value = HeaderValue::from_str(t).map_err(|e| {
90 AuthenticatorError::internal_error(
91 "Failed to set auth ticket".to_string(),
92 Some(e.to_string()),
93 )
94 })?;
95 headers.insert("auth-ticket", auth_ticket_header_value);
96 }
97 AuthHeaderManager::Custom(c) => c(headers, client)?,
98 AuthHeaderManager::CustomAsync(c) => c.set_headers(headers, client).await?,
99 }
100 Ok(())
101 }
102}
103
104pub struct AuthenticatorConfig {
106 pub client_id: String,
108 pub token_url: String,
110 pub secret: String,
112 pub resource: Option<String>,
114 pub audience: Option<String>,
116 pub scopes: Option<String>,
118 pub default_expires_in: Option<u64>,
123}
124
125#[derive(Serialize, Deserialize, Debug)]
126struct AuthenticatorRequest {
127 client_id: String,
128 client_secret: String,
129 resource: Option<String>,
130 audience: Option<String>,
131 scope: Option<String>,
132 grant_type: String,
133}
134
135impl AuthenticatorRequest {
136 fn new(config: AuthenticatorConfig) -> AuthenticatorRequest {
137 AuthenticatorRequest {
138 client_id: config.client_id,
139 client_secret: config.secret,
140 grant_type: "client_credentials".to_string(),
141 resource: config.resource,
142 audience: config.audience,
143 scope: config.scopes,
144 }
145 }
146}
147
148#[derive(Serialize, Deserialize, Debug)]
149struct AuthenticatorResponse {
150 access_token: String,
151 expires_in: Option<MaybeStringU64>,
152}
153
154#[derive(Serialize, Deserialize, Debug, Error)]
155pub struct AuthenticatorError {
157 pub error: String,
159 pub error_description: Option<String>,
161 pub error_uri: Option<String>,
163}
164
165impl AuthenticatorError {
166 pub fn internal_error(error: String, error_description: Option<String>) -> Self {
173 Self {
174 error,
175 error_description,
176 error_uri: None,
177 }
178 }
179}
180
181impl Display for AuthenticatorError {
182 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183 write!(f, "{}", self.error,)?;
184 if let Some(error_description) = &self.error_description {
185 write!(f, ": {error_description}")?;
186 }
187 if let Some(error_uri) = &self.error_uri {
188 write!(f, " ({error_uri})")?;
189 }
190 Ok(())
191 }
192}
193
194struct AuthenticatorState {
195 last_token: Option<String>,
196 current_token_expiry: Instant,
197}
198
199pub struct AuthenticatorResult {
201 token: String,
203 expiry: Instant,
205}
206
207pub struct Authenticator {
209 req: AuthenticatorRequest,
210 state: RwLock<AuthenticatorState>,
211 token_url: String,
212 default_expires_in: Option<Duration>,
213}
214
215impl AuthenticatorResult {
216 pub fn token(&self) -> &str {
218 &self.token
219 }
220
221 pub fn into_token(self) -> String {
223 self.token
224 }
225
226 pub fn expiry(&self) -> Instant {
228 self.expiry
229 }
230}
231
232impl Authenticator {
233 pub fn new(config: AuthenticatorConfig) -> Authenticator {
239 Authenticator {
240 token_url: config.token_url.clone(),
241 default_expires_in: config.default_expires_in.map(Duration::from_secs),
242 req: AuthenticatorRequest::new(config),
243 state: RwLock::new(AuthenticatorState {
244 last_token: None,
245 current_token_expiry: Instant::now(),
246 }),
247 }
248 }
249
250 async fn request_token(
251 &self,
252 client: &ClientWithMiddleware,
253 ) -> Result<AuthenticatorResult, AuthenticatorError> {
254 let response = client
255 .post(&self.token_url)
256 .form(&self.req)
257 .send()
258 .await
259 .map_err(|e| {
260 AuthenticatorError::internal_error(
261 "Something went wrong when sending the request".to_string(),
262 Some(e.to_string()),
263 )
264 })?;
265
266 let status = response.status();
267
268 let start = Instant::now();
269
270 let response = response.text().await.map_err(|e| {
271 AuthenticatorError::internal_error(
272 "Failed to receive response contents".to_owned(),
273 Some(e.to_string()),
274 )
275 })?;
276
277 if status != StatusCode::OK {
278 return match serde_json::from_str(&response) {
279 Ok(e) => Err(e),
280 Err(e) => Err(AuthenticatorError::internal_error(
281 format!("Something went wrong (status: {status}), but the response error couldn't be deserialized. Raw response: {response}")
282 , Some(e.to_string())))
283 };
284 }
285
286 let response: AuthenticatorResponse = serde_json::from_str(&response).map_err(|e| {
287 AuthenticatorError::internal_error(
288 "Failed to deserialize response from OAuth endpoint".to_string(),
289 Some(e.to_string()),
290 )
291 })?;
292
293 let token = response.access_token;
294 let Some(expires_in) = response
295 .expires_in
296 .map(|m| Duration::from_secs(m.0.saturating_sub(60)))
301 .or(self.default_expires_in)
302 else {
303 return Err(AuthenticatorError::internal_error(
304 "Missing expires_in in response, and no default expiration configured".to_owned(),
305 None,
306 ));
307 };
308
309 Ok(AuthenticatorResult {
310 token,
311 expiry: start + expires_in,
312 })
313 }
314
315 pub async fn get_token_with_expiry(
325 &self,
326 client: &ClientWithMiddleware,
327 ) -> Result<AuthenticatorResult, AuthenticatorError> {
328 let now = Instant::now();
329 {
330 let state = &*self.state.read().await;
331 if let Some(last) = &state.last_token {
332 if state.current_token_expiry > now {
333 return Ok(AuthenticatorResult {
334 token: last.clone(),
335 expiry: state.current_token_expiry,
336 });
337 }
338 }
339 }
340
341 let mut write = self.state.write().await;
343
344 if let Some(last) = &write.last_token {
347 if write.current_token_expiry > now {
348 return Ok(AuthenticatorResult {
349 token: last.clone(),
350 expiry: write.current_token_expiry,
351 });
352 }
353 }
354
355 match self.request_token(client).await {
356 Ok(response) => {
357 write.current_token_expiry = response.expiry;
358 write.last_token = Some(response.token.clone());
359 Ok(response)
360 }
361 Err(e) => Err(e),
362 }
363 }
364
365 pub async fn get_token(
372 &self,
373 client: &ClientWithMiddleware,
374 ) -> Result<String, AuthenticatorError> {
375 Ok(self.get_token_with_expiry(client).await?.token)
376 }
377}